Compare commits

...

174 Commits

Author SHA1 Message Date
q434343
1ab1ed69d4 Merge branch 'dev' into feat/lab_resource 2026-03-25 19:48:39 +08:00
q434343
ad2e5a1c04 更新prcxi的版面更新与工作流上传方法 2026-03-25 19:42:25 +08:00
q434343
04c0564366 Update .gitignore 2026-03-25 06:04:36 +08:00
q434343
9d65718f37 Merge branch 'prcix9320' into feat/lab_resource 2026-03-23 00:38:40 +08:00
q434343
35bcf6765d 修改rviz显示逻辑与joint_publisher,添加moveit2相关节点描述 2026-03-23 00:00:57 +08:00
q434343
cdbca70222 修改workflow上传逻辑,在trash初始化后再开始移液,修改枪头pick和drop的判断 2026-03-19 02:35:25 +08:00
q434343
1a267729e4 修改tracker的问题,在volumetracker报错时,禁用并进行下一步 2026-03-17 20:29:15 +08:00
q434343
b11f6eac55 修改pylabrobot更新后的影响 2026-03-16 20:42:13 +08:00
q434343
d85ff540c4 完成mix,liquid_hight,touch_tip,delay等参数的传递 2026-03-12 13:58:06 +08:00
q434343
5f45a0b81b 修改transfer liquid方法 2026-03-09 19:48:57 +08:00
Xuwznln
6bf9a319c7 Merge branch 'dev' into feat/lab_resource 2026-03-03 18:05:43 +08:00
Xuwznln
5c047beb83 support container as example
add z index

(cherry picked from commit 145fcaae65)
2026-03-03 18:04:13 +08:00
Xuwznln
b40c087143 fix container volume 2026-03-03 17:13:32 +08:00
q434343
74f0d5ee65 Merge branch 'feat/lab_resource' of https://github.com/deepmodeling/Uni-Lab-OS into feat/lab_resource 2026-03-03 14:17:36 +08:00
Xuwznln
7f1cc3b2a5 update materials 2026-03-03 11:43:52 +08:00
Xuwznln
2596d48a2f update materials 2026-03-03 11:43:41 +08:00
Xuwznln
3f160c2049 更新prcxi deck & 新增 unilabos_resource_slot 2026-03-03 11:40:23 +08:00
Xuwznln
2ac1a3242a 更新prcxi deck & 新增 unilabos_resource_slot 2026-03-03 11:40:02 +08:00
q434343
5d208c832b 修改工作流上传以及lh的物料初步判定 2026-03-02 18:32:44 +08:00
q434343
786498904d 修改上传方式,添加tip_rack的连线 2026-03-02 18:32:18 +08:00
q434343
a9ea9f425d 添加单枪头的多对多移液判定 2026-03-02 18:31:28 +08:00
Xuwznln
b3bc951cae registry update & workflow update 2026-03-02 18:31:26 +08:00
Xuwznln
01df4f1115 add resource 2026-03-02 18:30:07 +08:00
Xuwznln
a54e7c0f23 new workflow & prcxi slot removal 2026-03-02 18:29:25 +08:00
Xuwznln
e5015cd5e0 fix size change 2026-03-02 15:52:44 +08:00
Xuwznln
514373c164 v0.10.18
(cherry picked from commit 06b6f0d804)
2026-03-02 02:30:10 +08:00
q434343
41be9e4e19 merge lab_resource,并修改transfer liquid 2026-02-28 16:05:29 +08:00
Xuwznln
fcea02585a no opcua installation on macos 2026-02-28 09:41:37 +08:00
q434343
e1074f06d2 修改工作流上传以及lh的物料初步判定 2026-02-26 10:52:41 +08:00
q434343
0dc273f366 修改上传方式,添加tip_rack的连线 2026-02-24 19:37:11 +08:00
q434343
2e5fac26b3 添加单枪头的多对多移液判定 2026-02-13 13:46:27 +08:00
Xuwznln
07cf690897 fix possible crash 2026-02-12 01:46:26 +08:00
Xuwznln
cfea27460a fix deck & host_node 2026-02-12 01:46:24 +08:00
Xuwznln
b7d3e980a9 set liquid with tube 2026-02-12 01:46:23 +08:00
Xuwznln
5c2da9b793 fix possible crash 2026-02-11 23:44:53 +08:00
Xuwznln
45efbfcd12 fix deck & host_node 2026-02-11 17:33:26 +08:00
Xuwznln
8da6fdfd0b set liquid with tube 2026-02-11 16:20:07 +08:00
Xuwznln
29ea9909a5 Merge branch 'dev' into feat/lab_resource 2026-02-11 14:04:49 +08:00
Xuwznln
f9ed6cb3fb add test_resource_schema 2026-02-11 14:02:21 +08:00
q434343
f38f3dfc89 Merge branch 'dev' into prcix9320 2026-02-11 11:44:15 +08:00
Xuwznln
699a0b3ce7 fix test resource schema 2026-02-10 23:08:29 +08:00
Xuwznln
cf3a20ae79 registry update & workflow update 2026-02-10 22:46:07 +08:00
Xuwznln
ee6307a568 registry update & workflow update 2026-02-10 22:45:51 +08:00
Xuwznln
8a0116c852 add resource 2026-02-10 22:44:45 +08:00
Xuwznln
cdf0652020 add test mode 2026-02-10 15:18:41 +08:00
Xuwznln
60073ff139 support description & tags upload 2026-02-10 14:38:55 +08:00
q434343
d3f59913b0 Merge branch 'dev' into prcix9320 2026-02-10 14:15:36 +08:00
Xuwznln
a9053b822f fix config load 2026-02-10 13:06:05 +08:00
Xuwznln
d238c2ab8b fix log 2026-02-10 13:04:33 +08:00
Xuwznln
9a7d5c7c82 add registry name & add always free 2026-02-07 02:11:43 +08:00
q434343
f6d46e669d Merge branch 'dev' into prcix9320 2026-02-06 15:14:59 +08:00
q434343
abf5555e37 修改添加step mode 2026-02-06 15:12:56 +08:00
Xuwznln
4f7d431c0b correct config organic synthesis 2026-02-06 12:04:19 +08:00
Xuwznln
341a1b537c Adapt to new scheduler, sampels, and edge upload format (#230)
* add sample_material

* adapt to new samples sys

* fix pump transfer. fix resource update when protocol & ros callback

* Adapt to new scheduler.
2026-02-06 00:49:53 +08:00
Xuwznln
957fb41a6f Feat/samples (#229)
* add sample_material

* adapt to new samples sys
2026-02-05 00:42:12 +08:00
Xuwznln
26271bcab8 adapt to new samples sys 2026-02-04 18:49:08 +08:00
q434343
e4d915c59c Merge branch 'dev' into prcix9320 2026-02-04 15:46:16 +08:00
q434343
11a38d4558 添加加热震荡模块与磁力模块 2026-02-04 15:28:23 +08:00
Xuwznln
84a8223173 adapt to new edge format 2026-02-03 23:22:38 +08:00
Xuwznln
e8d1263488 workflow upload & prcxi transfer liquid 2026-02-03 18:10:32 +08:00
Xuwznln
380b39100d lh liquid 2026-02-03 15:15:57 +08:00
q434343
aeeb36d075 Merge branch 'dev' into prcix9320 2026-01-28 14:46:26 +08:00
q434343
3478bfd7ed 修改部分bug 2026-01-28 14:43:52 +08:00
q434343
d6910da57d 添加无物料9320 2026-01-26 21:07:03 +08:00
zhangshixiang
d5b4f07406 修改tube_rack的初始化,以及move_plate方法出现的物料增加问题 2026-01-22 18:45:42 +08:00
zhangshixiang
470d7283e4 修改消息转换 2026-01-19 15:15:38 +08:00
zhangshixiang
03f7f44c77 去除“屏蔽开机初始化” 2026-01-19 15:14:20 +08:00
zhangshixiang
6f600b4fc7 更新添加版位方法 2026-01-15 21:13:22 +08:00
zhangshixiang
269ce440d1 Merge branch 'dev' into prcix9320 2026-01-15 17:40:25 +08:00
zhangshixiang
be054589b5 更新自动化配置抓取位置 2026-01-15 17:21:14 +08:00
zhangshixiang
b045ab4e0a Revert "Merge pull request #214 from ALITTLELZ/prcxi1"
This reverts commit 4595f86725, reversing
changes made to 1340bae838.
2026-01-14 15:57:35 +08:00
q434343
4595f86725 Merge pull request #214 from ALITTLELZ/prcxi1
Prcxi1
2026-01-14 15:25:18 +08:00
ALITTLELZ
44a4c2362d Enhance PRCXI classes by adding category parameter and updating logic for channel handling; update resource tracking to include tube_rack category. 2026-01-14 15:14:48 +08:00
zhangshixiang
1340bae838 Revert "Merge branch 'dev' into prcix9320"
This reverts commit ae75f07c8e.
2026-01-13 18:33:32 +08:00
zhangshixiang
ae75f07c8e Merge branch 'dev' into prcix9320 2026-01-13 18:21:14 +08:00
zhangshixiang
18d0ba7a46 Revert "Merge branch 'dev' into prcix9320"
This reverts commit de7fbe7ac8.
2026-01-12 16:01:07 +08:00
zhangshixiang
de7fbe7ac8 Merge branch 'dev' into prcix9320 2026-01-12 14:31:56 +08:00
zhangshixiang
31e8d065c4 Merge branch 'dev' into prcix9320 2026-01-08 11:45:54 +08:00
zhangshixiang
219a480c08 merge prcxi.py 2026-01-07 15:32:27 +08:00
zhangshixiang
e9f1a7bb44 Merge branch 'dev' into prcix9320 2026-01-07 15:30:42 +08:00
zhangshixiang
ead43b2bc1 reverts edge上传相反
This reverts commit ad1312cf26.
2026-01-06 17:21:59 +08:00
q434343
cef86fd98d Merge pull request #210 from ALITTLELZ/prcxi9320
Update experiment JSON to change type from "plate" to "trash"
2026-01-04 15:09:38 +08:00
ALITTLELZ
6993e97ae9 Update experiment JSON to change type from "plate" to "trash" 2026-01-04 14:52:35 +08:00
Xianwei Qi
db396bcab3 test_one_to_multi 2025-12-31 19:15:27 +08:00
Junhan Chang
1fed8de57d example of beautifule tests 2025-12-31 17:09:09 +08:00
Xianwei Qi
63eb0c0a4c test_transfer_liquid_3
unilabos/test/experiments/prcxi_9320_with_res_test.json需要起的部分:
"debug": true,
"setup": false,
"simulator": true
2025-12-31 16:39:18 +08:00
q434343
888c6cf542 Merge remote-tracking branch 'origin/dev' into prcix9320 2025-12-30 16:20:05 +08:00
zhangshixiang
cc248fc32c Update prcxi.py 2025-12-28 21:25:08 +08:00
zhangshixiang
cfe64b023b 添加抓取后物料上传 2025-12-28 01:44:34 +08:00
zhangshixiang
ad1312cf26 修改传到网页的物料坐标 2025-12-28 01:40:20 +08:00
zhangshixiang
799813f85b Reapply "do not modify globally"
This reverts commit 19c9d655d0.
2025-12-26 14:16:44 +08:00
zhangshixiang
19c9d655d0 Revert "do not modify globally"
This reverts commit b0cff1a7a8.
2025-12-26 14:15:26 +08:00
Xuwznln
f9a9e35269 bump version to 0.10.3 2025-12-26 03:26:06 +08:00
Xuwznln
8cd306cd32 update registry 2025-12-26 02:57:03 +08:00
Xuwznln
816a0d747b update registry 2025-12-26 02:42:54 +08:00
Xuwznln
b0cff1a7a8 do not modify globally 2025-12-26 02:31:23 +08:00
zhangshixiang
71d57c5631 修改上传数据 2025-12-25 23:07:31 +08:00
q434343
546fb633ec Merge pull request #206 from ALITTLELZ/prcxi9320
Fix startup json for 9320
2025-12-25 16:50:25 +08:00
ALITTLELZ
a3c7fa9385 Enhance PRCXI9300 classes with new Container and TipRack implementations, improving state management and initialization logic. Update JSON configuration to reflect type changes for containers and plates. 2025-12-25 16:10:13 +08:00
zhangshixiang
c6cf84def0 Delete __init__.py 2025-12-25 14:59:31 +08:00
q434343
86512a0482 Merge pull request #204 from q434343/prcxi9320
Prcxi9320
2025-12-25 03:01:18 +08:00
q434343
3ddbc1c9b7 Merge branch 'prcix9320' into prcxi9320 2025-12-25 02:21:05 +08:00
zhangshixiang
abf1005241 云端可启动夹爪 2025-12-24 19:03:25 +08:00
zhangshixiang
c475eabb60 删除多余的laiyu部分 2025-12-23 19:09:00 +08:00
zhangshixiang
3ad20c85a5 夹爪添加 2025-12-23 14:54:36 +08:00
zhangshixiang
44fc80c70f 添加切换枪头方法,添加mock振荡与加热方法 2025-12-16 11:19:13 +08:00
zhangshixiang
8ba911bb55 修改根据新的物料格式,修改可视化 2025-12-10 15:10:53 +08:00
zhangshixiang
896f287d92 对laiyu移液站进行部分修改,取消多次初始化的问题 2025-12-10 15:10:15 +08:00
zhangshixiang
0d150f7acd 修改prcxijson,防止PlateT4的硬件错误 2025-12-09 19:33:59 +08:00
zhangshixiang
c27f7e42d6 修改prcxi的json文件,解决trach错误问题 2025-12-09 16:32:29 +08:00
zhangshixiang
cc56a68bc6 添加self.pending_liquids_dict的重置方法 2025-12-09 14:33:53 +08:00
zhangshixiang
d7302c3b35 返回dispense后的sample_uuid 2025-12-08 19:32:36 +08:00
zhangshixiang
b46a51c40e 添加 aspiate函数返回值 2025-12-08 16:55:38 +08:00
zhangshixiang
c6780087b8 修改host_node 2025-12-08 15:06:53 +08:00
zhangshixiang
1ef698dde6 修改pose标签设定机制 2025-12-06 16:20:49 +08:00
zhangshixiang
91aadba4ef 修改sample_uuid的返回值 2025-12-06 01:33:04 +08:00
Xuwznln
b1cdef9185 update version to 0.10.12 2025-12-04 18:47:16 +08:00
Xuwznln
9854ed8c9c fix ros2 future
print all logs to file
fix resource dict dump error
2025-12-04 18:46:37 +08:00
Xuwznln
52544a2c69 signal when host node is ready 2025-12-02 12:00:26 +08:00
ZiWei
5ce433e235 Fix startup with remote resource error
Resource dict fully change to "pose" key

Update oss link

Reduce pylabrobot conversion warning & force enable log dump.

更新 logo 图片
2025-12-02 11:51:01 +08:00
Xuwznln
c7c14d2332 Auto dump logs, fix workstation input schema 2025-11-27 14:24:40 +08:00
Harry Liu
6fdd482649 Transfer_liquid (#176)
* change 9320 desk row number to 4

* Updated 9320 host address

* Updated 9320 host address

* Add **kwargs in classes: PRCXI9300Deck and PRCXI9300Container

* Removed all sample_id in prcxi_9320.json to avoid KeyError

* 9320 machine testing settings

* Typo

* Typo in base_device_node.py

* Enhance liquid handling functionality by adding support for multiple transfer modes (one-to-many, one-to-one, many-to-one) and improving parameter validation. Default channel usage is set when not specified. Adjusted mixing logic to ensure it only occurs when valid conditions are met. Updated documentation for clarity.
2025-11-27 13:49:04 +08:00
Xuwznln
d390236318 Add get_regular_container func 2025-11-27 13:47:12 +08:00
Xuwznln
ed8ee29732 Add get_regular_container func 2025-11-27 13:46:40 +08:00
Xuwznln
ffc583e9d5 Add backend api and update doc 2025-11-26 19:17:46 +08:00
Xuwznln
f1ad0c9c96 Fix port error 2025-11-25 15:19:15 +08:00
Xuwznln
8fa3407649 Add result schema and add TypedDict conversion. 2025-11-25 15:16:27 +08:00
Xuwznln
d3282822fc add session_id and normal_exit 2025-11-20 22:43:24 +08:00
Xuwznln
554bcade24 Support unilabos_samples key 2025-11-19 15:53:59 +08:00
ZiWei
a662c75de1 feat(bioyond): 添加测量小瓶仓库和更新仓库工厂函数参数 2025-11-19 14:26:12 +08:00
ZiWei
931614fe64 feat(bioyond_studio): 添加项目API接口支持及优化物料管理功能
添加通用项目API接口方法(_post_project_api, _delete_project_api)用于与LIMS系统交互
实现compute_experiment_design方法用于实验设计计算
新增brief_step_parameters等订单相关接口方法
优化物料转移逻辑,增加异步任务处理
扩展BioyondV1RPC类,添加批量物料操作、订单状态管理等功能
2025-11-19 14:26:10 +08:00
Xuwznln
d39662f65f Update oss config 2025-11-19 14:22:03 +08:00
Xuwznln
acf5fdebf8 Add startup_json_path, disable_browser, port config 2025-11-18 18:59:39 +08:00
Xuwznln
7f7b1c13c0 bump version to 0.10.11 2025-11-18 18:47:26 +08:00
Xuwznln
75f09034ff update docs, test examples
fix liquid_handler init bug
2025-11-18 18:42:27 +08:00
ZiWei
549a50220b fix camera & workstation & warehouse & reaction station driver 2025-11-18 18:41:37 +08:00
Xuwznln
4189a2cfbe Add get_resource_with_dir & get_resource method 2025-11-15 22:50:30 +08:00
Xuwznln
48895a9bb1 Update repo files. 2025-11-15 03:15:44 +08:00
Xuwznln
891f126ed6 bump version to 0.10.10 2025-11-15 03:11:37 +08:00
Xuwznln
4d3475a849 Update devices 2025-11-15 03:11:36 +08:00
WenzheG
b475db66df nmr 2025-11-15 03:11:35 +08:00
ZiWei
a625a86e3e HR物料同步,前端展示位置修复 (#135)
* 更新Bioyond工作站配置,添加新的物料类型映射和载架定义,优化物料查询逻辑

* 添加Bioyond实验配置文件,定义物料类型映射和设备配置

* 更新bioyond_warehouse_reagent_stack方法,修正试剂堆栈尺寸和布局描述

* 更新Bioyond实验配置,修正物料类型映射,优化设备配置

* 更新Bioyond资源同步逻辑,优化物料入库流程,增强错误处理和日志记录

* 更新Bioyond资源,添加配液站和反应站专用载架,优化仓库工厂函数的排序方式

* 更新Bioyond资源,添加配液站和反应站相关载架,优化试剂瓶和样品瓶配置

* 更新Bioyond实验配置,修正试剂瓶载架ID,确保与设备匹配

* 更新Bioyond资源,移除反应站单烧杯载架,添加反应站单烧瓶载架分类

* Refactor Bioyond resource synchronization and update bottle carrier definitions

- Removed traceback printing in error handling for Bioyond synchronization.
- Enhanced logging for existing Bioyond material ID usage during synchronization.
- Added new bottle carrier definitions for single flask and updated existing ones.
- Refactored dispensing station and reaction station bottle definitions for clarity and consistency.
- Improved resource mapping and error handling in graphio for Bioyond resource conversion.
- Introduced layout parameter in warehouse factory for better warehouse configuration.

* 更新Bioyond仓库工厂,添加排序方式支持,优化坐标计算逻辑

* 更新Bioyond载架和甲板配置,调整样品板尺寸和仓库坐标

* 更新Bioyond资源同步,增强占用位置日志信息,修正坐标转换逻辑

* 更新Bioyond反应站和分配站配置,调整材料类型映射和ID,移除不必要的项

* support name change during materials change

* fix json dumps

* correct tip

* 优化调度器API路径,更新相关方法描述

* 更新 BIOYOND 载架相关文档,调整 API 以支持自带试剂瓶的载架类型,修复资源获取时的子物料处理逻辑

* 实现资源删除时的同步处理,优化出库操作逻辑

* 修复 ItemizedCarrier 中的可见性逻辑

* 保存 Bioyond 原始信息到 unilabos_extra,以便出库时查询

* 根据 resource.capacity 判断是试剂瓶(载架)还是多瓶载架,走不同的奔曜转换

* Fix bioyond bottle_carriers ordering

* 优化 Bioyond 物料同步逻辑,增强坐标解析和位置更新处理

* disable slave connect websocket

* correct remove_resource stats

* change uuid logger to trace level

* enable slave mode

* refactor(bioyond): 统一资源命名并优化物料同步逻辑

- 将DispensingStation和ReactionStation资源统一为PolymerStation命名
- 优化物料同步逻辑,支持耗材类型(typeMode=0)的查询
- 添加物料默认参数配置功能
- 调整仓库坐标布局
- 清理废弃资源定义

* feat(warehouses): 为仓库函数添加col_offset和layout参数

* refactor: 更新实验配置中的物料类型映射命名

将DispensingStation和ReactionStation的物料类型映射统一更名为PolymerStation,保持命名一致性

* fix: 更新实验配置中的载体名称从6VialCarrier到6StockCarrier

* feat(bioyond): 实现物料创建与入库分离逻辑

将物料同步流程拆分为两个独立阶段:transfer阶段只创建物料,add阶段执行入库
简化状态检查接口,仅返回连接状态

* fix(reaction_station): 修正液体进料烧杯体积单位并增强返回结果

将液体进料烧杯的体积单位从μL改为g以匹配实际使用场景
在返回结果中添加merged_workflow和order_params字段,提供更完整的工作流信息

* feat(dispensing_station): 在任务创建返回结果中添加order_params信息

在create_order方法返回结果中增加order_params字段,以便调用方获取完整的任务参数

* fix(dispensing_station): 修改90%物料分配逻辑从分成3份改为直接使用

原逻辑将主称固体平均分成3份作为90%物料,现改为直接使用main_portion

* feat(bioyond): 添加任务编码和任务ID的输出,支持批量任务创建后的状态监控

* refactor(registry): 简化设备配置中的任务结果处理逻辑

将多个单独的任务编码和ID字段合并为统一的return_info字段
更新相关描述以反映新的数据结构

* feat(工作站): 添加HTTP报送服务和任务完成状态跟踪

- 在graphio.py中添加API必需字段
- 实现工作站HTTP服务启动和停止逻辑
- 添加任务完成状态跟踪字典和等待方法
- 重写任务完成报送处理方法记录状态
- 支持批量任务完成等待和报告获取

* refactor(dispensing_station): 移除wait_for_order_completion_and_get_report功能

该功能已被wait_for_multiple_orders_and_get_reports替代,简化代码结构

* fix: 更新任务报告API错误

* fix(workstation_http_service): 修复状态查询中device_id获取逻辑

处理状态查询时安全获取device_id,避免因属性不存在导致的异常

* fix(bioyond_studio): 改进物料入库失败时的错误处理和日志记录

在物料入库API调用失败时,添加更详细的错误信息打印
同时修正station.py中对空响应和失败情况的判断逻辑

* refactor(bioyond): 优化瓶架载体的分配逻辑和注释说明

重构瓶架载体的分配逻辑,使用嵌套循环替代硬编码索引分配
添加更详细的坐标映射说明,明确PLR与Bioyond坐标的对应关系

* fix(bioyond_rpc): 修复物料入库成功时无data字段返回空的问题

当API返回成功但无data字段时,返回包含success标识的字典而非空字典

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
Co-authored-by: Junhan Chang <changjh@dp.tech>
2025-11-15 03:11:34 +08:00
xyc
37e0f1037c add new laiyu liquid driver, yaml and json files (#164) 2025-11-15 03:11:33 +08:00
tt
a242253145 标准化opcua设备接入unilab (#78)
* 初始提交,只保留工作区当前状态

* remove redundant arm_slider meshes

---------

Co-authored-by: Junhan Chang <changjh@dp.tech>
2025-11-15 03:11:31 +08:00
q434343
448e0074b7 3d sim (#97)
* 修改lh的json启动

* 修改lh的json启动

* 修改backend,做成sim的通用backend

* 修改yaml的地址,3D模型适配网页生产环境

* 添加laiyu硬件连接

* 修改移液枪的状态判断方法,

修改移液枪的状态判断方法,
添加三轴的表定点与零点之间的转换
添加三轴真实移动的backend

* 修改laiyu移液站

简化移动方法,
取消软件限制位置,
修改当值使用Z轴时也需要重新复位Z轴的问题

* 更新lh以及laiyu workshop

1,现在可以直接通过修改backend,适配其他的移液站,主类依旧使用LiquidHandler,不用重新编写

2,修改枪头判断标准,使用枪头自身判断而不是类的判断,

3,将归零参数用毫米计算,方便手动调整,

4,修改归零方式,上电使用机械归零,确定机械零点,手动归零设置工作区域零点方便计算,二者互不干涉

* 修改枪头动作

* 修改虚拟仿真方法

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: Junhan Chang <changjh@dp.tech>
2025-11-15 03:11:30 +08:00
lixinyu1011
304827fc8d 1114物料手册定义教程byxinyu (#165)
* 宜宾奔耀工站deck前端by_Xinyu

* 构建物料教程byxinyu

* 1114物料手册定义教程
2025-11-15 03:11:29 +08:00
Harry Liu
872b3d781f PRCXI Reset Error Correction (#166)
* change 9320 desk row number to 4

* Updated 9320 host address

* Updated 9320 host address

* Add **kwargs in classes: PRCXI9300Deck and PRCXI9300Container

* Removed all sample_id in prcxi_9320.json to avoid KeyError

* 9320 machine testing settings

* Typo

* Rewrite setup logic to clear error code

* 初始化 step_mode 属性
2025-11-15 03:11:29 +08:00
Xuwznln
813400f2b4 bump version to 0.10.9
update registry
2025-11-15 02:45:30 +08:00
Xuwznln
b6dfe2b944 Resource update & asyncio fix
correct bioyond config

prcxi example

fix append_resource

fix regularcontainer

fix cancel error

fix resource_get param

fix json dumps

support name change during materials change

enable slave mode

change uuid logger to trace level

correct remove_resource stats

disable slave connect websocket

adjust with_children param

modify devices to use correct executor (sleep, create_task)

support sleep and create_task in node

fix run async execution error
2025-11-15 02:45:12 +08:00
WenzheG
8807865649 添加Raman和xrd相关代码 2025-11-15 02:44:03 +08:00
Guangxin Zhang
5fc7eb7586 封膜仪、撕膜仪、耗材站接口 2025-11-15 02:44:02 +08:00
ZiWei
9bd72b48e1 Update workstation.
modify workstation_architecture docs

bioyond_HR (#133)

* feat: Enhance Bioyond synchronization and resource management

- Implemented synchronization for all material types (consumables, samples, reagents) from Bioyond, logging detailed information for each type.
- Improved error handling and logging during synchronization processes.
- Added functionality to save Bioyond material IDs in UniLab resources for future updates.
- Enhanced the `sync_to_external` method to handle material movements correctly, including querying and creating materials in Bioyond.
- Updated warehouse configurations to support new storage types and improved layout for better resource management.
- Introduced new resource types such as reactors and tip boxes, with detailed specifications.
- Modified warehouse factory to support column offsets for naming conventions (e.g., A05-D08).
- Improved resource tracking by merging extra attributes instead of overwriting them.
- Added a new method for updating resources in Bioyond, ensuring better synchronization of resource changes.

* feat: 添加TipBox和Reactor的配置到bottles.yaml

* fix: 修复液体投料方法中的volume参数处理逻辑

修复solid_feeding_vials方法中的volume参数处理逻辑,优化solvents参数的使用条件

更新液体投料方法,支持通过溶剂信息自动计算体积,添加solvents参数并更新文档描述

Add batch creation methods for vial and solution tasks

添加批量创建90%10%小瓶投料任务和二胺溶液配置任务的功能,更新相关参数和默认值
2025-11-15 02:43:50 +08:00
Xuwznln
42b78ab4c1 Update resource extra & uuid.
use ordering to convert identifier to idx

convert identifier to site idx

correct extra key

update extra before transfer

fix multiple instance error

add resource_tree_transfer func

fox itemrized carrier assign child resource

support internal device material transfer

remove extra key

use same callback group

support material extra

support material extra
support update_resource_site in extra
2025-11-15 02:43:13 +08:00
Xianwei Qi
9645609a05 PRCXI Update
修改prcxi连线

prcxi样例图

Create example_prcxi.json
2025-11-15 02:41:30 +08:00
ZiWei
a2a827d7ac Update workstation & bioyond example
Refine descriptions in Bioyond reaction station YAML

Updated and clarified field and operation descriptions in the reaction_station_bioyond.yaml file for improved accuracy and consistency. Changes include more precise terminology, clearer parameter explanations, and standardized formatting for operation schemas.

refactor(workstation): 更新反应站参数描述并添加分液站配置文件

修正反应站方法参数描述,使其更准确清晰
添加bioyond_dispensing_station.yaml配置文件

add create_workflow script and test

add invisible_slots to carriers

fix(warehouses): 修正bioyond_warehouse_1x4x4仓库的尺寸参数

调整仓库的num_items_x和num_items_z值以匹配实际布局,并更新物品尺寸参数

save resource get data. allow empty value for layout and cross_section_type

More decks&plates support for bioyond (#115)

refactor(registry): 重构反应站设备配置,简化并更新操作命令

移除旧的自动操作命令,新增针对具体化学操作的命令配置
更新模块路径和配置结构,优化参数定义和描述

fix(dispensing_station): 修正物料信息查询方法调用

将直接调用material_id_query改为通过hardware_interface调用,以符合接口设计规范
2025-11-15 02:40:54 +08:00
ZiWei
bb3ca645a4 Update graphio together with workstation design.
fix(reaction_station): 为步骤参数添加Value字段传个BY后端

fix(bioyond/warehouses): 修正仓库尺寸和物品排列参数

调整仓库的x轴和z轴物品数量以及物品尺寸参数,使其符合4x1x4的规格要求

fix warehouse serialize/deserialize

fix bioyond converter

fix itemized_carrier.unassign_child_resource

allow not-loaded MSG in registry

add layout serializer & converter

warehouseuse A1-D4; add warehouse layout

fix(graphio): 修正bioyond到plr资源转换中的坐标计算错误

Fix resource assignment and type mapping issues

Corrects resource assignment in ItemizedCarrier by using the correct spot key from _ordering. Updates graphio to use 'typeName' instead of 'name' for type mapping in resource_bioyond_to_plr. Renames DummyWorkstation to BioyondWorkstation in workstation_http_service for clarity.
2025-11-15 02:39:01 +08:00
Junhan Chang
37ee43d19a Update ResourceTracker
add more enumeration in POSE

fix converter in resource_tracker
2025-11-15 02:38:01 +08:00
Xuwznln
bc30f23e34 Update create_resource device_id 2025-10-20 21:45:20 +08:00
ZiWei
166d84afe1 fix(reaction_station): 清空工作流序列和参数避免重复执行 (#113)
在创建任务后清空工作流序列和参数,防止下次执行时累积重复
2025-10-17 13:44:36 +08:00
Junhan Chang
1b43c53015 fix resource_get in action 2025-10-17 13:44:35 +08:00
Xuwznln
d4415f5a35 Fix/update resource (#112)
* cancel upload_registry

* Refactor Bioyond workstation and experiment workflow -fix (#111)

* refactor(bioyond_studio): 优化材料缓存加载和参数验证逻辑

改进材料缓存加载逻辑以支持多种材料类型和详细材料处理
更新工作流参数验证中的字段名从key/value改为Key/DisplayValue
移除未使用的merge_workflow_with_parameters方法
添加get_station_info方法获取工作站基础信息
清理实验文件中的注释代码和更新导入路径

* fix: 修复资源移除时的父资源检查问题

在BaseROS2DeviceNode中,移除资源前添加对父资源是否为None的检查,避免空指针异常
同时更新Bottle和BottleCarrier类以支持**kwargs参数
修正测试文件中Liquid_feeding_beaker的大小写拼写错误

* correct return message

---------

Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
2025-10-17 03:08:15 +08:00
Xuwznln
0260cbbedb Close #107
Update doc url.
2025-10-16 17:26:45 +08:00
Xuwznln
7c440d10ab Fix/resource UUID and doc fix (#109)
* Fix ResourceTreeSet load error

* Raise error when using unsupported type to create ResourceTreeSet

* Fix children key error

* Fix children key error

* Fix workstation resource not tracking

* Fix workstation deck & children resource dupe

* Fix workstation deck & children resource dupe

* Fix multiple resource error

* Fix resource tree update

* Fix resource tree update

* Force confirm uuid

* Tip more error log

* Refactor Bioyond workstation and experiment workflow (#105)

Refactored the Bioyond workstation classes to improve parameter handling and workflow management. Updated experiment.py to use BioyondReactionStation with deck and material mappings, and enhanced workflow step parameter mapping and execution logic. Adjusted JSON experiment configs, improved workflow sequence handling, and added UUID assignment to PLR materials. Removed unused station_config and material cache logic, and added detailed docstrings and debug output for workflow methods.

* Fix resource get.
Fix resource parent not found.
Mapping uuid for all resources.

* mount parent uuid

* Add logging configuration based on BasicConfig in main function

* fix workstation node error

* fix workstation node error

* Update boot example

* temp fix for resource get

* temp fix for resource get

* provide error info when cant find plr type

* pack repo info

* fix to plr type error

* fix to plr type error

* Update regular container method

* support no size init

* fix comprehensive_station.json

* fix comprehensive_station.json

* fix type conversion

* fix state loading for regular container

* Update deploy-docs.yml

* Update deploy-docs.yml

---------

Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
2025-10-16 17:26:07 +08:00
Xuwznln
c85c49817d Fix workstation startup
Update registry
2025-10-13 15:06:30 +08:00
Xuwznln
c70eafa5f0 Fix one-key installation build for windows 2025-10-13 15:06:29 +08:00
Junhan Chang
b64466d443 modify default config 2025-10-13 15:06:26 +08:00
Junhan Chang
ef3f24ed48 add plr_to_bioyond, and refactor bioyond stations 2025-10-13 15:06:25 +08:00
Xuwznln
2a8e8d014b Fix conda pack on windows 2025-10-13 13:19:45 +08:00
Xuwznln
e0da1c7217 Fix one-key installation build
Install conda-pack before pack command

Add conda-pack to base when building one-key installer

Fix param error when using mamba run

Try fix one-key build on linux
2025-10-13 03:33:00 +08:00
hh.(SII)
51d3e61723 fix: rename schema field to resource_schema with serialization and validation aliases (#104)
Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
2025-10-13 03:24:20 +08:00
Xuwznln
6b5765bbf3 Complete all one key installation 2025-10-13 03:24:19 +08:00
Xuwznln
eb1f3fbe1c Try fix one-key build on linux 2025-10-13 02:10:05 +08:00
Xuwznln
fb93b1cd94 fix startup env check.
add auto install during one-key installation
2025-10-13 01:59:53 +08:00
Xuwznln
9aeffebde1 0.10.7 Update (#101)
* Cleanup registry to be easy-understanding (#76)

* delete deprecated mock devices

* rename categories

* combine chromatographic devices

* rename rviz simulation nodes

* organic virtual devices

* parse vessel_id

* run registry completion before merge

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>

* fix: workstation handlers and vessel_id parsing

* fix: working dir error when input config path
feat: report publish topic when error

* modify default discovery_interval to 15s

* feat: add trace log level

* feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79)

* fix: drop_tips not using auto resource select

* fix: discard_tips error

* fix: discard_tips

* fix: prcxi_res

* add: prcxi res
fix: startup slow

* feat: workstation example

* fix pumps and liquid_handler handle

* feat: 优化protocol node节点运行日志

* fix all protocol_compilers and remove deprecated devices

* feat: 新增use_remote_resource参数

* fix and remove redundant info

* bugfixes on organic protocols

* fix filter protocol

* fix protocol node

* 临时兼容错误的driver写法

* fix: prcxi import error

* use call_async in all service to avoid deadlock

* fix: figure_resource

* Update recipe.yaml

* add workstation template and battery example

* feat: add sk & ak

* update workstation base

* Create workstation_architecture.md

* refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode

* refactor: ProtocolNode→WorkstationNode

* Add:msgs.action (#83)

* update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84)

* Add:msgs.action

* update: 将版本号从 0.10.3 更新为 0.10.4

* simplify resource system

* uncompleted refactor

* example for use WorkstationBase

* feat: websocket

* feat: websocket test

* feat: workstation example

* feat: action status

* fix: station自己的方法注册错误

* fix: 还原protocol node处理方法

* fix: build

* fix: missing job_id key

* ws test version 1

* ws test version 2

* ws protocol

* 增加物料关系上传日志

* 增加物料关系上传日志

* 修正物料关系上传

* 修复工站的tracker实例追踪失效问题

* 增加handle检测,增加material edge关系上传

* 修复event loop错误

* 修复edge上报错误

* 修复async错误

* 更新schema的title字段

* 主机节点信息等支持自动刷新

* 注册表编辑器

* 修复status密集发送时,消息出错

* 增加addr参数

* fix: addr param

* fix: addr param

* 取消labid 和 强制config输入

* Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup

- Created LiquidHandlerSetGroup.action with fields for group name, wells, and volumes.
- Created LiquidHandlerTransferGroup.action with fields for source and target group names and unit volume.
- Both actions include response fields for return information and success status.

* Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists

* Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml

* result_info改为字典类型

* 新增uat的地址替换

* runze multiple pump support

(cherry picked from commit 49354fcf39)

* remove runze multiple software obtainer

(cherry picked from commit 8bcc92a394)

* support multiple backbone

(cherry picked from commit 4771ff2347)

* Update runze pump format

* Correct runze multiple backbone

* Update runze_multiple_backbone

* Correct runze pump multiple receive method.

* Correct runze pump multiple receive method.

* 对于PRCXI9320的transfer_group,一对多和多对多

* 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5

* fix import error

* fix dupe upload registry

* refactor ws client

* add server timeout

* Fix: run-column with correct vessel id (#86)

* fix run_column

* Update run_column_protocol.py

(cherry picked from commit e5aa4d940a)

* resource_update use resource_add

* 新增版位推荐功能

* 重新规定了版位推荐的入参

* update registry with nested obj

* fix protocol node log_message, added create_resource return value

* fix protocol node log_message, added create_resource return value

* try fix add protocol

* fix resource_add

* 修复移液站错误的aspirate注册表

* Feature/xprbalance-zhida (#80)

* feat(devices): add Zhida GC/MS pretreatment automation workstation

* feat(devices): add mettler_toledo xpr balance

* balance

* 重新补全zhida注册表

* PRCXI9320 json

* PRCXI9320 json

* PRCXI9320 json

* fix resource download

* remove class for resource

* bump version to 0.10.6

* 更新所有注册表

* 修复protocolnode的兼容性

* 修复protocolnode的兼容性

* Update install md

* Add Defaultlayout

* 更新物料接口

* fix dict to tree/nested-dict converter

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

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

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

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

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

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

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

* frontend_docs

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

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

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

* Workstation templates: Resources and its CRUD, and workstation tasks (#95)

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

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

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

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

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

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

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

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

---------

Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>

* 更新物料接口

* Workstation dev yb2 (#100)

* Refactor and extend reaction station action messages

* Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities

- Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability.
- Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input.
- Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input.
- Refined `create_diamine_solution_task` to include additional parameters for better task configuration.
- Enhanced schema descriptions and default values for improved user guidance.

* 修复to_plr_resources

* add update remove

* 支持选择器注册表自动生成
支持转运物料

* 修复资源添加

* 修复transfer_resource_to_another生成

* 更新transfer_resource_to_another参数,支持spot入参

* 新增test_resource动作

* fix host_node error

* fix host_node test_resource error

* fix host_node test_resource error

* 过滤本地动作

* 移动内部action以兼容host node

* 修复同步任务报错不显示的bug

* feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了

* update todo

* modify bioyond/plr converter, bioyond resource registry, and tests

* pass the tests

* update todo

* add conda-pack-build.yml

* add auto install script for conda-pack-build.yml

(cherry picked from commit 172599adcf)

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* Add version in __init__.py
Update conda-pack-build.yml
Add create_zip_archive.py

* Update conda-pack-build.yml

* Update conda-pack-build.yml (with mamba)

* Update conda-pack-build.yml

* Fix FileNotFoundError

* Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined>

* Fix unilabos msgs search error

* Fix environment_check.py

* Update recipe.yaml

* Update registry. Update uuid loop figure method. Update install docs.

* Fix nested conda pack

* Fix one-key installation path error

* Bump version to 0.10.7

* Workshop bj (#99)

* Add LaiYu Liquid device integration and tests

Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included.

* feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档

refactor: 重新组织LaiYu_Liquid模块目录结构
docs: 添加SOPA移液器和步进电机控制指令文档
fix: 修正设备配置中的最大体积默认值
test: 新增工作台配置测试用例
chore: 删除过时的测试脚本和配置文件

* add

* 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用

- 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py
- 更新所有相关文件中的导入引用
- 保持代码功能不变,仅改善命名一致性
- 测试确认所有导入正常工作

* 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出

- 添加 LaiYuLiquidBackend 到导入列表
- 添加 LaiYuLiquidBackend 到 __all__ 导出列表
- 确保所有主要类都可以正确导入

* 修复大小写文件夹名字

* 电池装配工站二次开发教程(带目录)上传至dev (#94)

* 电池装配工站二次开发教程

* Update intro.md

* 物料教程

* 更新物料教程,json格式注释

* Update prcxi driver & fix transfer_liquid mix_times (#90)

* Update prcxi driver & fix transfer_liquid mix_times

* fix: correct mix_times type

* Update liquid_handler registry

* test: prcxi.py

* Update registry from pr

* fix ony-key script not exist

* clean files

---------

Co-authored-by: Junhan Chang <changjh@dp.tech>
Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>
Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: LccLink <1951855008@qq.com>
Co-authored-by: lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com>
Co-authored-by: shiyubo0410 <shiyubo@dp.tech>
2025-10-12 23:34:26 +08:00
181 changed files with 46334 additions and 21931 deletions

View File

@@ -3,7 +3,7 @@
package: package:
name: unilabos name: unilabos
version: 0.10.17 version: 0.10.19
source: source:
path: ../../unilabos path: ../../unilabos
@@ -46,13 +46,15 @@ requirements:
- jinja2 - jinja2
- requests - requests
- uvicorn - uvicorn
- opcua - if: not osx
then:
- opcua
- pyserial - pyserial
- pandas - pandas
- pymodbus - pymodbus
- matplotlib - matplotlib
- pylibftdi - pylibftdi
- uni-lab::unilabos-env ==0.10.17 - uni-lab::unilabos-env ==0.10.19
about: about:
repository: https://github.com/deepmodeling/Uni-Lab-OS repository: https://github.com/deepmodeling/Uni-Lab-OS

View File

@@ -2,7 +2,7 @@
package: package:
name: unilabos-env name: unilabos-env
version: 0.10.17 version: 0.10.19
build: build:
noarch: generic noarch: generic

View File

@@ -3,7 +3,7 @@
package: package:
name: unilabos-full name: unilabos-full
version: 0.10.17 version: 0.10.19
build: build:
noarch: generic noarch: generic
@@ -11,7 +11,7 @@ build:
requirements: requirements:
run: run:
# Base unilabos package (includes unilabos-env) # Base unilabos package (includes unilabos-env)
- uni-lab::unilabos ==0.10.17 - uni-lab::unilabos ==0.10.19
# Documentation tools # Documentation tools
- sphinx - sphinx
- sphinx_rtd_theme - sphinx_rtd_theme

View File

@@ -0,0 +1,9 @@
@echo off
setlocal enabledelayedexpansion
REM upgrade pip
"%PREFIX%\python.exe" -m pip install --upgrade pip
REM install extra deps
"%PREFIX%\python.exe" -m pip install paho-mqtt opentrons_shared_data
"%PREFIX%\python.exe" -m pip install git+https://github.com/Xuwznln/pylabrobot.git

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euxo pipefail
# make sure pip is available
"$PREFIX/bin/python" -m pip install --upgrade pip
# install extra deps
"$PREFIX/bin/python" -m pip install paho-mqtt opentrons_shared_data
"$PREFIX/bin/python" -m pip install git+https://github.com/Xuwznln/pylabrobot.git

26
.cursorignore Normal file
View File

@@ -0,0 +1,26 @@
.conda
# .github
.idea
# .vscode
output
pylabrobot_repo
recipes
scripts
service
temp
# unilabos/test
# unilabos/app/web
unilabos/device_mesh
unilabos_data
unilabos_msgs
unilabos.egg-info
CONTRIBUTORS
# LICENSE
MANIFEST.in
pyrightconfig.json
# README.md
# README_zh.md
setup.py
setup.cfg
.gitattrubutes
**/__pycache__

View File

@@ -1,19 +0,0 @@
version: 2
updates:
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "dev"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
open-pull-requests-limit: 5
reviewers:
- "msgcenterpy-team"
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "ci"
include: "scope"

View File

@@ -49,7 +49,7 @@ jobs:
uv pip uninstall enum34 || echo enum34 not installed, skipping uv pip uninstall enum34 || echo enum34 not installed, skipping
uv pip install . uv pip install .
- name: Run check mode (complete_registry) - name: Run check mode (AST registry validation)
run: | run: |
call conda activate check-env call conda activate check-env
echo Running check mode... echo Running check mode...

3
.gitignore vendored
View File

@@ -5,6 +5,7 @@ output/
unilabos_data/ unilabos_data/
pyrightconfig.json pyrightconfig.json
.cursorignore .cursorignore
device_package*/
## Python ## Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
@@ -251,3 +252,5 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
test_config.py test_config.py
/.claude
/.cursor

87
AGENTS.md Normal file
View File

@@ -0,0 +1,87 @@
# AGENTS.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Also follow the monorepo-level rules in `../AGENTS.md`.
## Build & Development
```bash
# Install in editable mode (requires mamba env with python 3.11)
pip install -e .
uv pip install -r unilabos/utils/requirements.txt
# Run with a device graph
unilab --graph <graph.json> --config <config.py> --backend ros
unilab --graph <graph.json> --config <config.py> --backend simple # no ROS2 needed
# Common CLI flags
unilab --app_bridges websocket fastapi # communication bridges
unilab --test_mode # simulate hardware, no real execution
unilab --check_mode # CI validation of registry imports
unilab --skip_env_check # skip auto-install of dependencies
unilab --visual rviz|web|disable # visualization mode
unilab --is_slave # run as slave node
# Workflow upload subcommand
unilab workflow_upload -f <workflow.json> -n <name> --tags tag1 tag2
# Tests
pytest tests/ # all tests
pytest tests/resources/test_resourcetreeset.py # single test file
pytest tests/resources/test_resourcetreeset.py::TestClassName::test_method # single test
```
## Architecture
### Startup Flow
`unilab` CLI → `unilabos/app/main.py:main()` → loads config → builds registry → reads device graph (JSON/GraphML) → starts backend thread (ROS2/simple) → starts FastAPI web server + WebSocket client.
### Core Layers
**Registry** (`unilabos/registry/`): Singleton `Registry` class discovers and catalogs all device types, resource types, and communication devices from YAML definitions. Device types live in `registry/devices/*.yaml`, resources in `registry/resources/`, comms in `registry/device_comms/`. The registry resolves class paths to actual Python classes via `utils/import_manager.py`.
**Resource Tracking** (`unilabos/resources/resource_tracker.py`): Pydantic-based `ResourceDict``ResourceDictInstance``ResourceTreeSet` hierarchy. `ResourceTreeSet` is the canonical in-memory representation of all devices and resources, used throughout the system. Graph I/O is in `resources/graphio.py` (reads JSON/GraphML device topology files into `nx.Graph` + `ResourceTreeSet`).
**Device Drivers** (`unilabos/devices/`): 30+ hardware drivers organized by device type (liquid_handling, hplc, balance, arm, etc.). Each driver is a Python class that gets wrapped by `ros/device_node_wrapper.py:ros2_device_node()` to become a ROS2 node with publishers, subscribers, and action servers.
**ROS2 Layer** (`unilabos/ros/`): `device_node_wrapper.py` dynamically wraps any device class into `ROS2DeviceNode` (defined in `ros/nodes/base_device_node.py`). Preset node types in `ros/nodes/presets/` include `host_node`, `controller_node`, `workstation`, `serial_node`, `camera`. Messages use custom `unilabos_msgs` (pre-built, distributed via releases).
**Protocol Compilation** (`unilabos/compile/`): 20+ protocol compilers (add, centrifuge, dissolve, filter, heatchill, stir, pump, etc.) that transform YAML protocol definitions into executable sequences.
**Communication** (`unilabos/device_comms/`): Hardware communication adapters — OPC-UA client, Modbus PLC, RPC, and a universal driver. `app/communication.py` provides a factory pattern for WebSocket client connections to the cloud.
**Web/API** (`unilabos/app/web/`): FastAPI server with REST API (`api.py`), Jinja2 template pages (`pages.py`), and HTTP client for cloud communication (`client.py`). Runs on port 8002 by default.
### Configuration System
- **Config classes** in `unilabos/config/config.py`: `BasicConfig`, `WSConfig`, `HTTPConfig`, `ROSConfig` — all class-level attributes, loaded from Python config files
- Config files are `.py` files with matching class names (see `config/example_config.py`)
- Environment variables override with prefix `UNILABOS_` (e.g., `UNILABOS_BASICCONFIG_PORT=9000`)
- Device topology defined in graph files (JSON with node-link format, or GraphML)
### Key Data Flow
1. Graph file → `graphio.read_node_link_json()``(nx.Graph, ResourceTreeSet, resource_links)`
2. `ResourceTreeSet` + `Registry``initialize_device.initialize_device_from_dict()``ROS2DeviceNode` instances
3. Device nodes communicate via ROS2 topics/actions or direct Python calls (simple backend)
4. Cloud sync via WebSocket (`app/ws_client.py`) and HTTP (`app/web/client.py`)
### Test Data
Example device graphs and experiment configs are in `unilabos/test/experiments/` (not `tests/`). Registry test fixtures in `unilabos/test/registry/`.
## Code Conventions
- Code comments and log messages in simplified Chinese
- Python 3.11+, type hints expected
- Pydantic models for data validation (`resource_tracker.py`)
- Singleton pattern via `@singleton` decorator (`utils/decorator.py`)
- Dynamic class loading via `utils/import_manager.py` — device classes resolved at runtime from registry YAML paths
- CLI argument dashes auto-converted to underscores for consistency
## Licensing
- Framework code: GPL-3.0
- Device drivers (`unilabos/devices/`): DP Technology Proprietary License — do not redistribute

4
CLAUDE.md Normal file
View File

@@ -0,0 +1,4 @@
Please follow the rules defined in:
@AGENTS.md

View File

@@ -15,6 +15,9 @@ Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用,
**示例:** **示例:**
```python ```python
from unilabos.registry.decorators import device, topic_config
@device(id="mock_gripper", category=["gripper"], description="Mock Gripper")
class MockGripper: class MockGripper:
def __init__(self): def __init__(self):
self._position: float = 0.0 self._position: float = 0.0
@@ -23,19 +26,23 @@ class MockGripper:
self._status = "Idle" self._status = "Idle"
@property @property
@topic_config() # 添加 @topic_config 才会定时广播
def position(self) -> float: def position(self) -> float:
return self._position return self._position
@property @property
@topic_config()
def velocity(self) -> float: def velocity(self) -> float:
return self._velocity return self._velocity
@property @property
@topic_config()
def torque(self) -> float: def torque(self) -> float:
return self._torque return self._torque
# 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播 # 使用 @topic_config 装饰的属性,接入 Uni-Lab 时会定时对外广播
@property @property
@topic_config(period=2.0) # 可自定义发布周期
def status(self) -> str: def status(self) -> str:
return self._status return self._status
@@ -149,7 +156,7 @@ my_device: # 设备唯一标识符
系统会自动分析您的 Python 驱动类并生成: 系统会自动分析您的 Python 驱动类并生成:
- `status_types`:从 `@property` 装饰的方法自动识别状态属性 - `status_types`:从 `@topic_config` 装饰的 `@property` 方法自动识别状态属性
- `action_value_mappings`:从类方法自动生成动作映射 - `action_value_mappings`:从类方法自动生成动作映射
- `init_param_schema`:从 `__init__` 方法分析初始化参数 - `init_param_schema`:从 `__init__` 方法分析初始化参数
- `schema`:前端显示用的属性类型定义 - `schema`:前端显示用的属性类型定义
@@ -179,7 +186,9 @@ Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构:
```python ```python
from typing import Dict, Any from typing import Dict, Any
from unilabos.registry.decorators import device, topic_config
@device(id="my_device", category=["general"], description="My Device")
class MyDevice: class MyDevice:
"""设备类文档字符串 """设备类文档字符串
@@ -198,8 +207,9 @@ class MyDevice:
# 初始化硬件连接 # 初始化硬件连接
@property @property
@topic_config() # 必须添加 @topic_config 才会广播
def status(self) -> str: def status(self) -> str:
"""设备状态(会自动广播)""" """设备状态(通过 @topic_config 广播)"""
return self._status return self._status
def my_action(self, param: float) -> Dict[str, Any]: def my_action(self, param: float) -> Dict[str, Any]:
@@ -217,34 +227,61 @@ class MyDevice:
## 状态属性 vs 动作方法 ## 状态属性 vs 动作方法
### 状态属性(@property ### 状态属性(@property + @topic_config
状态属性会被自动识别并定期广播: 状态属性需要同时使用 `@property``@topic_config` 装饰器才会被识别并定期广播:
```python ```python
from unilabos.registry.decorators import topic_config
@property @property
@topic_config() # 必须添加,否则不会广播
def temperature(self) -> float: def temperature(self) -> float:
"""当前温度""" """当前温度"""
return self._read_temperature() return self._read_temperature()
@property @property
@topic_config(period=2.0) # 可自定义发布周期(秒)
def status(self) -> str: def status(self) -> str:
"""设备状态: idle, running, error""" """设备状态: idle, running, error"""
return self._status return self._status
@property @property
@topic_config(name="ready") # 可自定义发布名称
def is_ready(self) -> bool: def is_ready(self) -> bool:
"""设备是否就绪""" """设备是否就绪"""
return self._status == "idle" return self._status == "idle"
``` ```
也可以使用普通方法(非 @property)配合 `@topic_config`
```python
@topic_config(period=10.0)
def get_sensor_data(self) -> Dict[str, float]:
"""获取传感器数据get_ 前缀会自动去除,发布名为 sensor_data"""
return {"temp": self._temp, "humidity": self._humidity}
```
**`@topic_config` 参数**:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `period` | float | 5.0 | 发布周期(秒) |
| `print_publish` | bool | 节点默认 | 是否打印发布日志 |
| `qos` | int | 10 | QoS 深度 |
| `name` | str | None | 自定义发布名称 |
**发布名称优先级**`@topic_config(name=...)` > `get_` 前缀去除 > 方法名
**特点**: **特点**:
- 使用`@property`装饰器 - 必须使用 `@topic_config` 装饰器
- 只读,不能有参数 - 支持 `@property` 和普通方法
- 自动添加到注册表的`status_types` - 添加到注册表的 `status_types`
- 定期发布到 ROS2 topic - 定期发布到 ROS2 topic
> **⚠️ 重要:** 仅有 `@property` 装饰器而没有 `@topic_config` 的属性**不会**被广播。这是一个 Breaking Change。
### 动作方法 ### 动作方法
动作方法是设备可以执行的操作: 动作方法是设备可以执行的操作:
@@ -497,6 +534,7 @@ class LiquidHandler:
self._status = "idle" self._status = "idle"
@property @property
@topic_config()
def status(self) -> str: def status(self) -> str:
return self._status return self._status
@@ -886,7 +924,52 @@ class MyDevice:
## 最佳实践 ## 最佳实践
### 1. 类型注解 ### 1. 使用 `@device` 装饰器标识设备
```python
from unilabos.registry.decorators import device
@device(id="my_device", category=["heating"], description="My Heating Device", icon="heater.webp")
class MyDevice:
...
```
- `id`:设备唯一标识符,用于注册表匹配
- `category`:分类列表,前端用于分组显示
- `description`:设备描述
- `icon`:图标文件名(可选)
### 2. 使用 `@topic_config` 声明需要广播的状态
```python
from unilabos.registry.decorators import topic_config
# ✓ @property + @topic_config → 会广播
@property
@topic_config(period=2.0)
def temperature(self) -> float:
return self._temp
# ✓ 普通方法 + @topic_config → 会广播get_ 前缀自动去除)
@topic_config(period=10.0)
def get_sensor_data(self) -> Dict[str, float]:
return {"temp": self._temp}
# ✓ 使用 name 参数自定义发布名称
@property
@topic_config(name="ready")
def is_ready(self) -> bool:
return self._status == "idle"
# ✗ 仅有 @property没有 @topic_config → 不会广播
@property
def internal_state(self) -> str:
return self._state
```
> **注意:** 与 `@property` 连用时,`@topic_config` 必须放在 `@property` 下面。
### 3. 类型注解
```python ```python
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
@@ -901,7 +984,7 @@ def method(
pass pass
``` ```
### 2. 文档字符串 ### 4. 文档字符串
```python ```python
def method(self, param: float) -> Dict[str, Any]: def method(self, param: float) -> Dict[str, Any]:
@@ -923,7 +1006,7 @@ def method(self, param: float) -> Dict[str, Any]:
pass pass
``` ```
### 3. 配置验证 ### 5. 配置验证
```python ```python
def __init__(self, config: Dict[str, Any]): def __init__(self, config: Dict[str, Any]):
@@ -937,7 +1020,7 @@ def __init__(self, config: Dict[str, Any]):
self.baudrate = config['baudrate'] self.baudrate = config['baudrate']
``` ```
### 4. 资源清理 ### 6. 资源清理
```python ```python
def __del__(self): def __del__(self):
@@ -946,7 +1029,7 @@ def __del__(self):
self.connection.close() self.connection.close()
``` ```
### 5. 设计前端友好的返回值 ### 7. 设计前端友好的返回值
**记住:返回值会直接显示在 Web 界面** **记住:返回值会直接显示在 Web 界面**

View File

@@ -422,18 +422,20 @@ placeholder_keys:
### status_types ### status_types
系统会扫描你的 Python 类,从状态方法property 或 get\_方法自动生成这部分: 系统会扫描你的 Python 类,从带有 `@topic_config` 装饰器的 `@property`方法自动生成这部分:
```yaml ```yaml
status_types: status_types:
current_temperature: float # 从 get_current_temperature() 或 @property current_temperature current_temperature: float # 从 @topic_config 装饰的 @property 或方法
is_heating: bool # 从 get_is_heating() 或 @property is_heating is_heating: bool
status: str # 从 get_status() 或 @property status status: str
``` ```
**注意事项** **注意事项**
- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性 - 仅有带 `@topic_config` 装饰器的 `@property` 或方法才会被识别为状态属性
- 没有 `@topic_config``@property` 不会生成 status_types也不会广播
- `get_` 前缀的方法名会自动去除前缀(如 `get_temperature``temperature`
- 类型会自动转成相应的类型(如 `str``float``bool` - 类型会自动转成相应的类型(如 `str``float``bool`
- 如果类型是 `Any``None` 或未知的,默认使用 `String` - 如果类型是 `Any``None` 或未知的,默认使用 `String`
@@ -537,11 +539,13 @@ class AdvancedLiquidHandler:
self._temperature = 25.0 self._temperature = 25.0
@property @property
@topic_config()
def status(self) -> str: def status(self) -> str:
"""设备状态""" """设备状态"""
return self._status return self._status
@property @property
@topic_config()
def temperature(self) -> float: def temperature(self) -> float:
"""当前温度""" """当前温度"""
return self._temperature return self._temperature
@@ -809,21 +813,23 @@ my_temperature_controller:
你的设备类需要符合以下要求: 你的设备类需要符合以下要求:
```python ```python
from unilabos.common.device_base import DeviceBase from unilabos.registry.decorators import device, topic_config
class MyDevice(DeviceBase): @device(id="my_device", category=["temperature"], description="My Device")
class MyDevice:
def __init__(self, config): def __init__(self, config):
"""初始化,参数会自动分析到 init_param_schema.config""" """初始化,参数会自动分析到 init_param_schema.config"""
super().__init__(config)
self.port = config.get('port', '/dev/ttyUSB0') self.port = config.get('port', '/dev/ttyUSB0')
# 状态方法(会自动生成到 status_types # 状态方法(必须添加 @topic_config 才会生成到 status_types 并广播
@property @property
@topic_config()
def status(self): def status(self):
"""返回设备状态""" """返回设备状态"""
return "idle" return "idle"
@property @property
@topic_config()
def temperature(self): def temperature(self):
"""返回当前温度""" """返回当前温度"""
return 25.0 return 25.0
@@ -1039,7 +1045,34 @@ resource.type # "resource"
### 代码规范 ### 代码规范
1. **始终使用类型注解** 1. **使用 `@device` 装饰器标识设备类**
```python
from unilabos.registry.decorators import device
@device(id="my_device", category=["heating"], description="My Device")
class MyDevice:
...
```
2. **使用 `@topic_config` 声明广播属性**
```python
from unilabos.registry.decorators import topic_config
# ✓ 需要广播的状态属性
@property
@topic_config(period=2.0)
def temperature(self) -> float:
return self._temp
# ✗ 仅有 @property 不会广播
@property
def internal_counter(self) -> int:
return self._counter
```
3. **始终使用类型注解**
```python ```python
# ✓ 好 # ✓ 好
@@ -1051,7 +1084,7 @@ def method(self, resource, device):
pass pass
``` ```
2. **提供有意义的参数名** 4. **提供有意义的参数名**
```python ```python
# ✓ 好 - 清晰的参数名 # ✓ 好 - 清晰的参数名
@@ -1063,7 +1096,7 @@ def transfer(self, r1: ResourceSlot, r2: ResourceSlot):
pass pass
``` ```
3. **使用 Optional 表示可选参数** 5. **使用 Optional 表示可选参数**
```python ```python
from typing import Optional from typing import Optional
@@ -1076,7 +1109,7 @@ def method(
pass pass
``` ```
4. **添加详细的文档字符串** 6. **添加详细的文档字符串**
```python ```python
def method( def method(
@@ -1096,13 +1129,13 @@ def method(
pass pass
``` ```
5. **方法命名规范** 7. **方法命名规范**
- 状态方法使用 `@property` 装饰器或 `get_` 前缀 - 状态方法使用 `@property` + `@topic_config` 装饰器,或普通方法 + `@topic_config`
- 动作方法使用动词开头 - 动作方法使用动词开头
- 保持命名清晰、一致 - 保持命名清晰、一致
6. **完善的错误处理** 8. **完善的错误处理**
- 实现完善的错误处理 - 实现完善的错误处理
- 添加日志记录 - 添加日志记录
- 提供有意义的错误信息 - 提供有意义的错误信息

View File

@@ -221,10 +221,10 @@ Laboratory A Laboratory B
```bash ```bash
# 实验室A # 实验室A
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource unilab --ak your_ak --sk your_sk --upload_registry
# 实验室B # 实验室B
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource unilab --ak your_ak --sk your_sk --upload_registry
``` ```
--- ---

File diff suppressed because it is too large Load Diff

View File

@@ -452,8 +452,9 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
**操作步骤:** **操作步骤:**
1. 将两个 `container` 拖拽到 `workstation` 1. 将两个 `container` 拖拽到 `workstation`
2.`virtual_transfer_pump` 拖拽到 `workstation` 2.`virtual_multiway_valve` 拖拽到 `workstation`
3. 在画布上连接它们(建立父子关系) 3. `virtual_transfer_pump` 拖拽到 `workstation`
4. 在画布上连接它们(建立父子关系)
![设备连接](image/links.png) ![设备连接](image/links.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 415 KiB

View File

@@ -22,7 +22,6 @@ options:
--is_slave Run the backend as slave node (without host privileges). --is_slave Run the backend as slave node (without host privileges).
--slave_no_host Skip waiting for host service in slave mode --slave_no_host Skip waiting for host service in slave mode
--upload_registry Upload registry information when starting unilab --upload_registry Upload registry information when starting unilab
--use_remote_resource Use remote resources when starting unilab
--config CONFIG Configuration file path, supports .py format Python config files --config CONFIG Configuration file path, supports .py format Python config files
--port PORT Port for web service information page --port PORT Port for web service information page
--disable_browser Disable opening information page on startup --disable_browser Disable opening information page on startup
@@ -85,7 +84,7 @@ Uni-Lab 的启动过程分为以下几个阶段:
支持两种方式: 支持两种方式:
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式) - **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
- **远程资源**使用 `--use_remote_resource` 从云端获取 - **远程资源**不指定本地文件即可
### 7. 注册表构建 ### 7. 注册表构建
@@ -196,7 +195,7 @@ unilab --config path/to/your/config.py
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
# 使用远程资源启动 # 使用远程资源启动
unilab --ak your_ak --sk your_sk --use_remote_resource unilab --ak your_ak --sk your_sk
# 更新注册表 # 更新注册表
unilab --ak your_ak --sk your_sk --complete_registry unilab --ak your_ak --sk your_sk --complete_registry

View File

@@ -1,6 +1,6 @@
package: package:
name: ros-humble-unilabos-msgs name: ros-humble-unilabos-msgs
version: 0.10.17 version: 0.10.19
source: source:
path: ../../unilabos_msgs path: ../../unilabos_msgs
target_directory: src target_directory: src

View File

@@ -1,6 +1,6 @@
package: package:
name: unilabos name: unilabos
version: "0.10.17" version: "0.10.19"
source: source:
path: ../.. path: ../..

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup( setup(
name=package_name, name=package_name,
version='0.10.17', version='0.10.19',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=['setuptools'], install_requires=['setuptools'],

View File

@@ -0,0 +1,15 @@
# Liquid handling 集成测试
`test_transfer_liquid.py` 现在会调用 PRCXI 的 RViz 仿真 backend运行前请确保
1. 已安装包含 `pylabrobot``rclpy` 的运行环境;
2. 启动 ROS 依赖(`rviz` 可选,但是 `rviz_backend` 会创建 ROS 节点);
3. 在 shell 中设置 `UNILAB_SIM_TEST=1`,否则 pytest 会自动跳过这些慢速用例:
```bash
export UNILAB_SIM_TEST=1
pytest tests/devices/liquid_handling/test_transfer_liquid.py -m slow
```
如果只需验证逻辑层(不依赖仿真),可以直接运行 `tests/devices/liquid_handling/unit_test.py`,该文件使用 Fake backend适合作为 CI 的快速测试。***

View File

@@ -0,0 +1,547 @@
import asyncio
from dataclasses import dataclass
from typing import Any, Iterable, List, Optional, Sequence, Tuple
import pytest
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
@dataclass(frozen=True)
class DummyContainer:
name: str
def __repr__(self) -> str: # pragma: no cover
return f"DummyContainer({self.name})"
@dataclass(frozen=True)
class DummyTipSpot:
name: str
def __repr__(self) -> str: # pragma: no cover
return f"DummyTipSpot({self.name})"
def make_tip_iter(n: int = 256) -> Iterable[List[DummyTipSpot]]:
"""Yield lists so code can safely call `tip.extend(next(self.current_tip))`."""
for i in range(n):
yield [DummyTipSpot(f"tip_{i}")]
class FakeLiquidHandler(LiquidHandlerAbstract):
"""不初始化真实 backend/deck仅用来记录 transfer_liquid 内部调用序列。"""
def __init__(self, channel_num: int = 8):
# 不调用 super().__init__避免真实硬件/后端依赖
self.channel_num = channel_num
self.support_touch_tip = True
self.current_tip = iter(make_tip_iter())
self.calls: List[Tuple[str, Any]] = []
async def pick_up_tips(self, tip_spots, use_channels=None, offsets=None, **backend_kwargs):
self.calls.append(("pick_up_tips", {"tips": list(tip_spots), "use_channels": use_channels}))
async def aspirate(
self,
resources: Sequence[Any],
vols: List[float],
use_channels: Optional[List[int]] = None,
flow_rates: Optional[List[Optional[float]]] = None,
offsets: Any = None,
liquid_height: Any = None,
blow_out_air_volume: Any = None,
spread: str = "wide",
**backend_kwargs,
):
self.calls.append(
(
"aspirate",
{
"resources": list(resources),
"vols": list(vols),
"use_channels": list(use_channels) if use_channels is not None else None,
"flow_rates": list(flow_rates) if flow_rates is not None else None,
"offsets": list(offsets) if offsets is not None else None,
"liquid_height": list(liquid_height) if liquid_height is not None else None,
"blow_out_air_volume": list(blow_out_air_volume) if blow_out_air_volume is not None else None,
},
)
)
async def dispense(
self,
resources: Sequence[Any],
vols: List[float],
use_channels: Optional[List[int]] = None,
flow_rates: Optional[List[Optional[float]]] = None,
offsets: Any = None,
liquid_height: Any = None,
blow_out_air_volume: Any = None,
spread: str = "wide",
**backend_kwargs,
):
self.calls.append(
(
"dispense",
{
"resources": list(resources),
"vols": list(vols),
"use_channels": list(use_channels) if use_channels is not None else None,
"flow_rates": list(flow_rates) if flow_rates is not None else None,
"offsets": list(offsets) if offsets is not None else None,
"liquid_height": list(liquid_height) if liquid_height is not None else None,
"blow_out_air_volume": list(blow_out_air_volume) if blow_out_air_volume is not None else None,
},
)
)
async def discard_tips(self, use_channels=None, *args, **kwargs):
# 有的分支是 discard_tips(use_channels=[0]),有的分支是 discard_tips([0..7])(位置参数)
self.calls.append(("discard_tips", {"use_channels": list(use_channels) if use_channels is not None else None}))
async def custom_delay(self, seconds=0, msg=None):
self.calls.append(("custom_delay", {"seconds": seconds, "msg": msg}))
async def touch_tip(self, targets):
# 原实现会访问 targets.get_size_x() 等;测试里只记录调用
self.calls.append(("touch_tip", {"targets": targets}))
def run(coro):
return asyncio.run(coro)
def test_one_to_one_single_channel_basic_calls():
lh = FakeLiquidHandler(channel_num=1)
lh.current_tip = iter(make_tip_iter(64))
sources = [DummyContainer(f"S{i}") for i in range(3)]
targets = [DummyContainer(f"T{i}") for i in range(3)]
run(
lh.transfer_liquid(
sources=sources,
targets=targets,
tip_racks=[],
use_channels=[0],
asp_vols=[1, 2, 3],
dis_vols=[4, 5, 6],
mix_times=None, # 应该仍能执行(不 mix
)
)
assert [c[0] for c in lh.calls].count("pick_up_tips") == 3
assert [c[0] for c in lh.calls].count("aspirate") == 3
assert [c[0] for c in lh.calls].count("dispense") == 3
assert [c[0] for c in lh.calls].count("discard_tips") == 3
# 每次 aspirate/dispense 都是单孔列表
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
assert aspirates[0]["resources"] == [sources[0]]
assert aspirates[0]["vols"] == [1.0]
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert dispenses[2]["resources"] == [targets[2]]
assert dispenses[2]["vols"] == [6.0]
def test_one_to_one_single_channel_before_stage_mixes_prior_to_aspirate():
lh = FakeLiquidHandler(channel_num=1)
lh.current_tip = iter(make_tip_iter(16))
source = DummyContainer("S0")
target = DummyContainer("T0")
run(
lh.transfer_liquid(
sources=[source],
targets=[target],
tip_racks=[],
use_channels=[0],
asp_vols=[5],
dis_vols=[5],
mix_stage="before",
mix_times=1,
mix_vol=3,
)
)
aspirate_calls = [(idx, payload) for idx, (name, payload) in enumerate(lh.calls) if name == "aspirate"]
assert len(aspirate_calls) >= 2
mix_idx, mix_payload = aspirate_calls[0]
assert mix_payload["resources"] == [target]
assert mix_payload["vols"] == [3]
transfer_idx, transfer_payload = aspirate_calls[1]
assert transfer_payload["resources"] == [source]
assert mix_idx < transfer_idx
def test_one_to_one_eight_channel_groups_by_8():
lh = FakeLiquidHandler(channel_num=8)
lh.current_tip = iter(make_tip_iter(256))
sources = [DummyContainer(f"S{i}") for i in range(16)]
targets = [DummyContainer(f"T{i}") for i in range(16)]
asp_vols = list(range(1, 17))
dis_vols = list(range(101, 117))
run(
lh.transfer_liquid(
sources=sources,
targets=targets,
tip_racks=[],
use_channels=list(range(8)),
asp_vols=asp_vols,
dis_vols=dis_vols,
mix_times=0, # 触发逻辑但不 mix
)
)
# 16 个任务 -> 2 组,每组 8 通道一起做
assert [c[0] for c in lh.calls].count("pick_up_tips") == 2
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert len(aspirates) == 2
assert len(dispenses) == 2
assert aspirates[0]["resources"] == sources[0:8]
assert aspirates[0]["vols"] == [float(v) for v in asp_vols[0:8]]
assert dispenses[1]["resources"] == targets[8:16]
assert dispenses[1]["vols"] == [float(v) for v in dis_vols[8:16]]
def test_one_to_one_eight_channel_requires_multiple_of_8_targets():
lh = FakeLiquidHandler(channel_num=8)
lh.current_tip = iter(make_tip_iter(64))
sources = [DummyContainer(f"S{i}") for i in range(9)]
targets = [DummyContainer(f"T{i}") for i in range(9)]
with pytest.raises(ValueError, match="multiple of 8"):
run(
lh.transfer_liquid(
sources=sources,
targets=targets,
tip_racks=[],
use_channels=list(range(8)),
asp_vols=[1] * 9,
dis_vols=[1] * 9,
mix_times=0,
)
)
def test_one_to_one_eight_channel_parameter_lists_are_chunked_per_8():
lh = FakeLiquidHandler(channel_num=8)
lh.current_tip = iter(make_tip_iter(512))
sources = [DummyContainer(f"S{i}") for i in range(16)]
targets = [DummyContainer(f"T{i}") for i in range(16)]
asp_vols = [i + 1 for i in range(16)]
dis_vols = [200 + i for i in range(16)]
asp_flow_rates = [0.1 * (i + 1) for i in range(16)]
dis_flow_rates = [0.2 * (i + 1) for i in range(16)]
offsets = [f"offset_{i}" for i in range(16)]
liquid_heights = [i * 0.5 for i in range(16)]
blow_out_air_volume = [i + 0.05 for i in range(16)]
run(
lh.transfer_liquid(
sources=sources,
targets=targets,
tip_racks=[],
use_channels=list(range(8)),
asp_vols=asp_vols,
dis_vols=dis_vols,
asp_flow_rates=asp_flow_rates,
dis_flow_rates=dis_flow_rates,
offsets=offsets,
liquid_height=liquid_heights,
blow_out_air_volume=blow_out_air_volume,
mix_times=0,
)
)
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert len(aspirates) == len(dispenses) == 2
for batch_idx in range(2):
start = batch_idx * 8
end = start + 8
asp_call = aspirates[batch_idx]
dis_call = dispenses[batch_idx]
assert asp_call["resources"] == sources[start:end]
assert asp_call["flow_rates"] == asp_flow_rates[start:end]
assert asp_call["offsets"] == offsets[start:end]
assert asp_call["liquid_height"] == liquid_heights[start:end]
assert asp_call["blow_out_air_volume"] == blow_out_air_volume[start:end]
assert dis_call["flow_rates"] == dis_flow_rates[start:end]
assert dis_call["offsets"] == offsets[start:end]
assert dis_call["liquid_height"] == liquid_heights[start:end]
assert dis_call["blow_out_air_volume"] == blow_out_air_volume[start:end]
def test_one_to_one_eight_channel_handles_32_tasks_four_batches():
lh = FakeLiquidHandler(channel_num=8)
lh.current_tip = iter(make_tip_iter(1024))
sources = [DummyContainer(f"S{i}") for i in range(32)]
targets = [DummyContainer(f"T{i}") for i in range(32)]
asp_vols = [i + 1 for i in range(32)]
dis_vols = [300 + i for i in range(32)]
run(
lh.transfer_liquid(
sources=sources,
targets=targets,
tip_racks=[],
use_channels=list(range(8)),
asp_vols=asp_vols,
dis_vols=dis_vols,
mix_times=0,
)
)
pick_calls = [name for name, _ in lh.calls if name == "pick_up_tips"]
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert len(pick_calls) == 4
assert len(aspirates) == len(dispenses) == 4
assert aspirates[0]["resources"] == sources[0:8]
assert aspirates[-1]["resources"] == sources[24:32]
assert dispenses[0]["resources"] == targets[0:8]
assert dispenses[-1]["resources"] == targets[24:32]
def test_one_to_many_single_channel_aspirates_total_when_asp_vol_too_small():
lh = FakeLiquidHandler(channel_num=1)
lh.current_tip = iter(make_tip_iter(64))
source = DummyContainer("SRC")
targets = [DummyContainer(f"T{i}") for i in range(3)]
dis_vols = [10, 20, 30] # sum=60
run(
lh.transfer_liquid(
sources=[source],
targets=targets,
tip_racks=[],
use_channels=[0],
asp_vols=10, # 小于 sum(dis_vols) -> 应吸 60
dis_vols=dis_vols,
mix_times=0,
)
)
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
assert len(aspirates) == 1
assert aspirates[0]["resources"] == [source]
assert aspirates[0]["vols"] == [60.0]
assert aspirates[0]["use_channels"] == [0]
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert [d["vols"][0] for d in dispenses] == [10.0, 20.0, 30.0]
def test_one_to_many_eight_channel_basic():
lh = FakeLiquidHandler(channel_num=8)
lh.current_tip = iter(make_tip_iter(128))
source = DummyContainer("SRC")
targets = [DummyContainer(f"T{i}") for i in range(8)]
dis_vols = [i + 1 for i in range(8)]
run(
lh.transfer_liquid(
sources=[source],
targets=targets,
tip_racks=[],
use_channels=list(range(8)),
asp_vols=999, # one-to-many 8ch 会按 dis_vols 吸(每通道各自)
dis_vols=dis_vols,
mix_times=0,
)
)
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
assert aspirates[0]["resources"] == [source] * 8
assert aspirates[0]["vols"] == [float(v) for v in dis_vols]
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert dispenses[0]["resources"] == targets
assert dispenses[0]["vols"] == [float(v) for v in dis_vols]
def test_many_to_one_single_channel_standard_dispense_equals_asp_by_default():
lh = FakeLiquidHandler(channel_num=1)
lh.current_tip = iter(make_tip_iter(128))
sources = [DummyContainer(f"S{i}") for i in range(3)]
target = DummyContainer("T")
asp_vols = [5, 6, 7]
run(
lh.transfer_liquid(
sources=sources,
targets=[target],
tip_racks=[],
use_channels=[0],
asp_vols=asp_vols,
dis_vols=1, # many-to-one 允许标量;非比例模式下实际每次分液=对应 asp_vol
mix_times=0,
)
)
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert [d["vols"][0] for d in dispenses] == [float(v) for v in asp_vols]
assert all(d["resources"] == [target] for d in dispenses)
def test_many_to_one_single_channel_before_stage_mixes_target_once():
lh = FakeLiquidHandler(channel_num=1)
lh.current_tip = iter(make_tip_iter(128))
sources = [DummyContainer("S0"), DummyContainer("S1")]
target = DummyContainer("T")
run(
lh.transfer_liquid(
sources=sources,
targets=[target],
tip_racks=[],
use_channels=[0],
asp_vols=[5, 6],
dis_vols=1,
mix_stage="before",
mix_times=2,
mix_vol=4,
)
)
aspirate_calls = [(idx, payload) for idx, (name, payload) in enumerate(lh.calls) if name == "aspirate"]
assert len(aspirate_calls) >= 1
mix_idx, mix_payload = aspirate_calls[0]
assert mix_payload["resources"] == [target]
assert mix_payload["vols"] == [4]
# 第一個 mix 之後會真正開始吸 source
assert any(call["resources"] == [sources[0]] for _, call in aspirate_calls[1:])
def test_many_to_one_single_channel_proportional_mixing_uses_dis_vols_per_source():
lh = FakeLiquidHandler(channel_num=1)
lh.current_tip = iter(make_tip_iter(128))
sources = [DummyContainer(f"S{i}") for i in range(3)]
target = DummyContainer("T")
asp_vols = [5, 6, 7]
dis_vols = [1, 2, 3]
run(
lh.transfer_liquid(
sources=sources,
targets=[target],
tip_racks=[],
use_channels=[0],
asp_vols=asp_vols,
dis_vols=dis_vols, # 比例模式
mix_times=0,
)
)
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert [d["vols"][0] for d in dispenses] == [float(v) for v in dis_vols]
def test_many_to_one_eight_channel_basic():
lh = FakeLiquidHandler(channel_num=8)
lh.current_tip = iter(make_tip_iter(256))
sources = [DummyContainer(f"S{i}") for i in range(8)]
target = DummyContainer("T")
asp_vols = [10 + i for i in range(8)]
run(
lh.transfer_liquid(
sources=sources,
targets=[target],
tip_racks=[],
use_channels=list(range(8)),
asp_vols=asp_vols,
dis_vols=999, # 非比例模式下每通道分液=对应 asp_vol
mix_times=0,
)
)
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert aspirates[0]["resources"] == sources
assert aspirates[0]["vols"] == [float(v) for v in asp_vols]
assert dispenses[0]["resources"] == [target] * 8
assert dispenses[0]["vols"] == [float(v) for v in asp_vols]
def test_transfer_liquid_mode_detection_unsupported_shape_raises():
lh = FakeLiquidHandler(channel_num=8)
lh.current_tip = iter(make_tip_iter(64))
sources = [DummyContainer("S0"), DummyContainer("S1")]
targets = [DummyContainer("T0"), DummyContainer("T1"), DummyContainer("T2")]
with pytest.raises(ValueError, match="Unsupported transfer mode"):
run(
lh.transfer_liquid(
sources=sources,
targets=targets,
tip_racks=[],
use_channels=[0],
asp_vols=[1, 1],
dis_vols=[1, 1, 1],
mix_times=0,
)
)
def test_mix_single_target_produces_matching_cycles():
lh = FakeLiquidHandler(channel_num=1)
target = DummyContainer("T_mix")
run(lh.mix(targets=[target], mix_time=2, mix_vol=5))
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
assert len(aspirates) == len(dispenses) == 2
assert all(call["resources"] == [target] for call in aspirates)
assert all(call["vols"] == [5] for call in aspirates)
assert all(call["resources"] == [target] for call in dispenses)
assert all(call["vols"] == [5] for call in dispenses)
def test_mix_multiple_targets_supports_per_target_offsets():
lh = FakeLiquidHandler(channel_num=1)
targets = [DummyContainer("T0"), DummyContainer("T1")]
offsets = ["left", "right"]
heights = [0.1, 0.2]
rates = [0.5, 1.0]
run(
lh.mix(
targets=targets,
mix_time=1,
mix_vol=3,
offsets=offsets,
height_to_bottom=heights,
mix_rate=rates,
)
)
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
assert len(aspirates) == 2
assert aspirates[0]["resources"] == [targets[0]]
assert aspirates[0]["offsets"] == [offsets[0]]
assert aspirates[0]["liquid_height"] == [heights[0]]
assert aspirates[0]["flow_rates"] == [rates[0]]
assert aspirates[1]["resources"] == [targets[1]]
assert aspirates[1]["offsets"] == [offsets[1]]
assert aspirates[1]["liquid_height"] == [heights[1]]
assert aspirates[1]["flow_rates"] == [rates[1]]

View File

@@ -1 +1 @@
__version__ = "0.10.17" __version__ = "0.10.19"

View File

@@ -1,6 +0,0 @@
"""Entry point for `python -m unilabos`."""
from unilabos.app.main import main
if __name__ == "__main__":
main()

View File

@@ -1,8 +1,10 @@
import argparse import argparse
import asyncio import asyncio
import os import os
import platform
import shutil import shutil
import signal import signal
import subprocess
import sys import sys
import threading import threading
import time import time
@@ -24,6 +26,84 @@ from unilabos.config.config import load_config, BasicConfig, HTTPConfig
_restart_requested: bool = False _restart_requested: bool = False
_restart_reason: str = "" _restart_reason: str = ""
RESTART_EXIT_CODE = 42
def _build_child_argv():
"""Build sys.argv for child process, stripping supervisor-only arguments."""
result = []
skip_next = False
for arg in sys.argv:
if skip_next:
skip_next = False
continue
if arg in ("--restart_mode", "--restart-mode"):
continue
if arg in ("--auto_restart_count", "--auto-restart-count"):
skip_next = True
continue
if arg.startswith("--auto_restart_count=") or arg.startswith("--auto-restart-count="):
continue
result.append(arg)
return result
def _run_as_supervisor(max_restarts: int):
"""
Supervisor process that spawns and monitors child processes.
Similar to Uvicorn's --reload: the supervisor itself does no heavy work,
it only launches the real process as a child and restarts it when the child
exits with RESTART_EXIT_CODE.
"""
child_argv = [sys.executable] + _build_child_argv()
restart_count = 0
print_status(
f"[Supervisor] Restart mode enabled (max restarts: {max_restarts}), "
f"child command: {' '.join(child_argv)}",
"info",
)
while True:
print_status(
f"[Supervisor] Launching process (restart {restart_count}/{max_restarts})...",
"info",
)
try:
process = subprocess.Popen(child_argv)
exit_code = process.wait()
except KeyboardInterrupt:
print_status("[Supervisor] Interrupted, terminating child process...", "info")
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
sys.exit(1)
if exit_code == RESTART_EXIT_CODE:
restart_count += 1
if restart_count > max_restarts:
print_status(
f"[Supervisor] Maximum restart count ({max_restarts}) reached, exiting",
"warning",
)
sys.exit(1)
print_status(
f"[Supervisor] Child requested restart ({restart_count}/{max_restarts}), restarting in 2s...",
"info",
)
time.sleep(2)
else:
if exit_code != 0:
print_status(f"[Supervisor] Child exited with code {exit_code}", "warning")
else:
print_status("[Supervisor] Child exited normally", "info")
sys.exit(exit_code)
def load_config_from_file(config_path): def load_config_from_file(config_path):
if config_path is None: if config_path is None:
@@ -65,6 +145,13 @@ def parse_args():
action="append", action="append",
help="Path to the registry directory", help="Path to the registry directory",
) )
parser.add_argument(
"--devices",
type=str,
default=None,
action="append",
help="Path to Python code directory for AST-based device/resource scanning",
)
parser.add_argument( parser.add_argument(
"--working_dir", "--working_dir",
type=str, type=str,
@@ -154,23 +241,53 @@ def parse_args():
action="store_true", action="store_true",
help="Skip environment dependency check on startup", help="Skip environment dependency check on startup",
) )
parser.add_argument(
"--complete_registry",
action="store_true",
default=False,
help="Complete registry information",
)
parser.add_argument( parser.add_argument(
"--check_mode", "--check_mode",
action="store_true", action="store_true",
default=False, default=False,
help="Run in check mode for CI: validates registry imports and ensures no file changes", help="Run in check mode for CI: validates registry imports and ensures no file changes",
) )
parser.add_argument(
"--complete_registry",
action="store_true",
default=False,
help="Complete and rewrite YAML registry files using AST analysis results",
)
parser.add_argument( parser.add_argument(
"--no_update_feedback", "--no_update_feedback",
action="store_true", action="store_true",
help="Disable sending update feedback to server", help="Disable sending update feedback to server",
) )
parser.add_argument(
"--test_mode",
action="store_true",
default=False,
help="Test mode: all actions simulate execution and return mock results without running real hardware",
)
parser.add_argument(
"--external_devices_only",
action="store_true",
default=False,
help="Only load external device packages (--devices), skip built-in unilabos/devices/ scanning and YAML device registry",
)
parser.add_argument(
"--extra_resource",
action="store_true",
default=False,
help="Load extra lab_ prefixed labware resources (529 auto-generated definitions from lab_resources.py)",
)
parser.add_argument(
"--restart_mode",
action="store_true",
default=False,
help="Enable supervisor mode: automatically restart the process when triggered via WebSocket",
)
parser.add_argument(
"--auto_restart_count",
type=int,
default=500,
help="Maximum number of automatic restarts in restart mode (default: 500)",
)
# workflow upload subcommand # workflow upload subcommand
workflow_parser = subparsers.add_parser( workflow_parser = subparsers.add_parser(
"workflow_upload", "workflow_upload",
@@ -204,6 +321,12 @@ def parse_args():
default=False, default=False,
help="Whether to publish the workflow (default: False)", help="Whether to publish the workflow (default: False)",
) )
workflow_parser.add_argument(
"--description",
type=str,
default="",
help="Workflow description, used when publishing the workflow",
)
return parser return parser
@@ -215,68 +338,88 @@ def main():
args = parser.parse_args() args = parser.parse_args()
args_dict = vars(args) args_dict = vars(args)
# Supervisor mode: spawn child processes and monitor for restart
if args_dict.get("restart_mode", False):
_run_as_supervisor(args_dict.get("auto_restart_count", 5))
return
# 环境检查 - 检查并自动安装必需的包 (可选) # 环境检查 - 检查并自动安装必需的包 (可选)
skip_env_check = args_dict.get("skip_env_check", False) skip_env_check = args_dict.get("skip_env_check", False)
check_mode = args_dict.get("check_mode", False) check_mode = args_dict.get("check_mode", False)
if not skip_env_check: if not skip_env_check:
from unilabos.utils.environment_check import check_environment from unilabos.utils.environment_check import check_environment, check_device_package_requirements
if not check_environment(auto_install=True): if not check_environment(auto_install=True):
print_status("环境检查失败,程序退出", "error") print_status("环境检查失败,程序退出", "error")
os._exit(1) os._exit(1)
# 第一次设备包依赖检查build_registry 之前,确保 import map 可用
devices_dirs_for_req = args_dict.get("devices", None)
if devices_dirs_for_req:
if not check_device_package_requirements(devices_dirs_for_req):
print_status("设备包依赖检查失败,程序退出", "error")
os._exit(1)
else: else:
print_status("跳过环境依赖检查", "warning") print_status("跳过环境依赖检查", "warning")
# 加载配置文件优先加载config然后从env读取 # 加载配置文件优先加载config然后从env读取
config_path = args_dict.get("config") config_path = args_dict.get("config")
if check_mode: # === 解析 working_dir ===
args_dict["working_dir"] = os.path.abspath(os.getcwd()) # 规则1: working_dir 传入 → 检测 unilabos_data 子目录,已是则不修改
# 当 skip_env_check 时,默认使用当前目录作为 working_dir # 规则2: 仅 config_path 传入 → 用其父目录作为 working_dir
if skip_env_check and not args_dict.get("working_dir") and not config_path: # 规则4: 两者都传入 → 各用各的,但 working_dir 仍做 unilabos_data 子目录检测
raw_working_dir = args_dict.get("working_dir")
if raw_working_dir:
working_dir = os.path.abspath(raw_working_dir)
elif config_path and os.path.exists(config_path):
working_dir = os.path.dirname(os.path.abspath(config_path))
else:
working_dir = os.path.abspath(os.getcwd()) working_dir = os.path.abspath(os.getcwd())
print_status(f"跳过环境检查模式:使用当前目录作为工作目录 {working_dir}", "info")
# 检查当前目录是否有 local_config.py # unilabos_data 子目录自动检测
local_config_in_cwd = os.path.join(working_dir, "local_config.py") if os.path.basename(working_dir) != "unilabos_data":
if os.path.exists(local_config_in_cwd): unilabos_data_sub = os.path.join(working_dir, "unilabos_data")
config_path = local_config_in_cwd if os.path.isdir(unilabos_data_sub):
working_dir = unilabos_data_sub
elif not raw_working_dir and not (config_path and os.path.exists(config_path)):
# 未显式指定路径,默认使用 cwd/unilabos_data
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
# === 解析 config_path ===
if config_path and not os.path.exists(config_path):
# config_path 传入但不存在,尝试在 working_dir 中查找
candidate = os.path.join(working_dir, "local_config.py")
if os.path.exists(candidate):
config_path = candidate
print_status(f"在工作目录中发现配置文件: {config_path}", "info")
else:
print_status(
f"配置文件 {config_path} 不存在,工作目录 {working_dir} 中也未找到 local_config.py"
f"请通过 --config 传入 local_config.py 文件路径",
"error",
)
os._exit(1)
elif not config_path:
# 规则3: 未传入 config_path尝试 working_dir/local_config.py
candidate = os.path.join(working_dir, "local_config.py")
if os.path.exists(candidate):
config_path = candidate
print_status(f"发现本地配置文件: {config_path}", "info") print_status(f"发现本地配置文件: {config_path}", "info")
else: else:
print_status(f"未指定config路径可通过 --config 传入 local_config.py 文件路径", "info") print_status(f"未指定config路径可通过 --config 传入 local_config.py 文件路径", "info")
elif os.getcwd().endswith("unilabos_data"): print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
working_dir = os.path.abspath(os.getcwd()) if check_mode or input() != "n":
else: os.makedirs(working_dir, exist_ok=True)
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data")) config_path = os.path.join(working_dir, "local_config.py")
shutil.copy(
if args_dict.get("working_dir"): os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"),
working_dir = args_dict.get("working_dir", "") config_path,
if config_path and not os.path.exists(config_path):
config_path = os.path.join(working_dir, "local_config.py")
if not os.path.exists(config_path):
print_status(
f"当前工作目录 {working_dir} 未找到local_config.py请通过 --config 传入 local_config.py 文件路径",
"error",
) )
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
else:
os._exit(1) os._exit(1)
elif config_path and os.path.exists(config_path):
working_dir = os.path.dirname(config_path)
elif os.path.exists(working_dir) and os.path.exists(os.path.join(working_dir, "local_config.py")):
config_path = os.path.join(working_dir, "local_config.py")
elif not skip_env_check and not config_path and (
not os.path.exists(working_dir) or not os.path.exists(os.path.join(working_dir, "local_config.py"))
):
print_status(f"未指定config路径可通过 --config 传入 local_config.py 文件路径", "info")
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
if input() != "n":
os.makedirs(working_dir, exist_ok=True)
config_path = os.path.join(working_dir, "local_config.py")
shutil.copy(
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
)
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
else:
os._exit(1)
# 加载配置文件 (check_mode 跳过) # 加载配置文件 (check_mode 跳过)
print_status(f"当前工作目录为 {working_dir}", "info") print_status(f"当前工作目录为 {working_dir}", "info")
@@ -288,7 +431,9 @@ def main():
if hasattr(BasicConfig, "log_level"): if hasattr(BasicConfig, "log_level"):
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.") logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir) file_path = configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
if file_path is not None:
logger.info(f"[LOG_FILE] {file_path}")
if args.addr != parser.get_default("addr"): if args.addr != parser.get_default("addr"):
if args.addr == "test": if args.addr == "test":
@@ -332,46 +477,66 @@ def main():
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False) BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
BasicConfig.upload_registry = args_dict.get("upload_registry", False) BasicConfig.upload_registry = args_dict.get("upload_registry", False)
BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False) BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False)
BasicConfig.test_mode = args_dict.get("test_mode", False)
if BasicConfig.test_mode:
print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning")
BasicConfig.extra_resource = args_dict.get("extra_resource", False)
if BasicConfig.extra_resource:
print_status("启用额外资源加载将加载lab_开头的labware资源定义", "info")
BasicConfig.communication_protocol = "websocket" BasicConfig.communication_protocol = "websocket"
machine_name = os.popen("hostname").read().strip() machine_name = platform.node()
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
BasicConfig.machine_name = machine_name BasicConfig.machine_name = machine_name
BasicConfig.vis_2d_enable = args_dict["2d_vis"] BasicConfig.vis_2d_enable = args_dict["2d_vis"]
BasicConfig.check_mode = check_mode BasicConfig.check_mode = check_mode
from unilabos.resources.graphio import (
read_node_link_json,
read_graphml,
dict_from_graph,
)
from unilabos.app.communication import get_communication_client
from unilabos.registry.registry import build_registry from unilabos.registry.registry import build_registry
from unilabos.app.backend import start_backend
from unilabos.app.web import http_client
from unilabos.app.web import start_server
from unilabos.app.register import register_devices_and_resources
from unilabos.resources.graphio import modify_to_backend_format
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
# 显示启动横幅 # 显示启动横幅
print_unilab_banner(args_dict) print_unilab_banner(args_dict)
# 注册表 - check_mode 时强制启用 complete_registry # Step 0: AST 分析优先 + YAML 注册表加载
# check_mode 和 upload_registry 都会执行实际 import 验证
devices_dirs = args_dict.get("devices", None)
complete_registry = args_dict.get("complete_registry", False) or check_mode complete_registry = args_dict.get("complete_registry", False) or check_mode
lab_registry = build_registry(args_dict["registry_path"], complete_registry, BasicConfig.upload_registry) external_only = args_dict.get("external_devices_only", False)
lab_registry = build_registry(
registry_paths=args_dict["registry_path"],
devices_dirs=devices_dirs,
upload_registry=BasicConfig.upload_registry,
check_mode=check_mode,
complete_registry=complete_registry,
external_only=external_only,
)
# Check mode: complete_registry 完成后直接退出git diff 检测由 CI workflow 执行 # Check mode: 注册表验证完成后直接退出
if check_mode: if check_mode:
print_status("Check mode: complete_registry 完成,退出", "info") device_count = len(lab_registry.device_type_registry)
resource_count = len(lab_registry.resource_type_registry)
print_status(f"Check mode: 注册表验证完成 ({device_count} 设备, {resource_count} 资源),退出", "info")
os._exit(0) os._exit(0)
# 以下导入依赖 ROS2 环境check_mode 已退出不需要
from unilabos.resources.graphio import (
read_node_link_json,
read_graphml,
dict_from_graph,
modify_to_backend_format,
)
from unilabos.app.communication import get_communication_client
from unilabos.app.backend import start_backend
from unilabos.app.web import http_client
from unilabos.app.web import start_server
from unilabos.app.register import register_devices_and_resources
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
# Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data
if BasicConfig.upload_registry: if BasicConfig.upload_registry:
# 设备注册到服务端 - 需要 ak 和 sk
if BasicConfig.ak and BasicConfig.sk: if BasicConfig.ak and BasicConfig.sk:
print_status("开始注册设备到服务端...", "info") # print_status("开始注册设备到服务端...", "info")
try: try:
register_devices_and_resources(lab_registry) register_devices_and_resources(lab_registry)
print_status("设备注册完成", "info") # print_status("设备注册完成", "info")
except Exception as e: except Exception as e:
print_status(f"设备注册失败: {e}", "error") print_status(f"设备注册失败: {e}", "error")
else: else:
@@ -456,12 +621,16 @@ def main():
continue continue
# 如果从远端获取了物料信息,则与本地物料进行同步 # 如果从远端获取了物料信息,则与本地物料进行同步
if request_startup_json and "nodes" in request_startup_json: if file_path is not None and request_startup_json and "nodes" in request_startup_json:
print_status("开始同步远端物料到本地...", "info") print_status("开始同步远端物料到本地...", "info")
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"]) remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
resource_tree_set.merge_remote_resources(remote_tree_set) resource_tree_set.merge_remote_resources(remote_tree_set)
print_status("远端物料同步完成", "info") print_status("远端物料同步完成", "info")
# 第二次设备包依赖检查云端物料同步后community 包可能引入新的 requirements
# TODO: 当 community device package 功能上线后,在这里调用
# install_requirements_txt(community_pkg_path / "requirements.txt", label="community.xxx")
# 使用 ResourceTreeSet 代替 list # 使用 ResourceTreeSet 代替 list
args_dict["resources_config"] = resource_tree_set args_dict["resources_config"] = resource_tree_set
args_dict["devices_config"] = resource_tree_set args_dict["devices_config"] = resource_tree_set
@@ -553,6 +722,10 @@ def main():
open_browser=not args_dict["disable_browser"], open_browser=not args_dict["disable_browser"],
port=BasicConfig.port, port=BasicConfig.port,
) )
if restart_requested:
print_status("[Main] Restart requested, cleaning up...", "info")
cleanup_for_restart()
os._exit(RESTART_EXIT_CODE)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -54,6 +54,7 @@ class JobAddReq(BaseModel):
action_type: str = Field( action_type: str = Field(
examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action type", default="" examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action type", default=""
) )
sample_material: dict = Field(examples=[{"string": "string"}], description="sample uuid to material uuid")
action_args: dict = Field(examples=[{"string": "string"}], description="action arguments", default_factory=dict) action_args: dict = Field(examples=[{"string": "string"}], description="action arguments", default_factory=dict)
task_id: str = Field(examples=["task_id"], description="task uuid (auto-generated if empty)", default="") task_id: str = Field(examples=["task_id"], description="task uuid (auto-generated if empty)", default="")
job_id: str = Field(examples=["job_id"], description="goal uuid (auto-generated if empty)", default="") job_id: str = Field(examples=["job_id"], description="goal uuid (auto-generated if empty)", default="")

View File

@@ -1,9 +1,8 @@
import json
import time import time
from typing import Optional, Tuple, Dict, Any from typing import Any, Dict, Optional, Tuple
from unilabos.utils.log import logger from unilabos.utils.log import logger
from unilabos.utils.type_check import TypeEncoder from unilabos.utils.tools import normalize_json as _normalize_device
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]: def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
@@ -11,50 +10,63 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[
注册设备和资源到服务器仅支持HTTP 注册设备和资源到服务器仅支持HTTP
""" """
# 注册资源信息 - 使用HTTP方式
from unilabos.app.web.client import http_client from unilabos.app.web.client import http_client
logger.info("[UniLab Register] 开始注册设备和资源...") logger.info("[UniLab Register] 开始注册设备和资源...")
# 注册设备信息
devices_to_register = {} devices_to_register = {}
for device_info in lab_registry.obtain_registry_device_info(): for device_info in lab_registry.obtain_registry_device_info():
devices_to_register[device_info["id"]] = json.loads( devices_to_register[device_info["id"]] = _normalize_device(device_info)
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder) logger.trace(f"[UniLab Register] 收集设备: {device_info['id']}")
)
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
resources_to_register = {} resources_to_register = {}
for resource_info in lab_registry.obtain_registry_resource_info(): for resource_info in lab_registry.obtain_registry_resource_info():
resources_to_register[resource_info["id"]] = resource_info resources_to_register[resource_info["id"]] = resource_info
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}") logger.trace(f"[UniLab Register] 收集资源: {resource_info['id']}")
if gather_only: if gather_only:
return devices_to_register, resources_to_register return devices_to_register, resources_to_register
# 注册设备
if devices_to_register: if devices_to_register:
try: try:
start_time = time.time() start_time = time.time()
response = http_client.resource_registry({"resources": list(devices_to_register.values())}) response = http_client.resource_registry(
{"resources": list(devices_to_register.values())},
tag="device_registry",
)
cost_time = time.time() - start_time cost_time = time.time() - start_time
if response.status_code in [200, 201]: res_data = response.json() if response.status_code == 200 else {}
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}ms") skipped = res_data.get("data", {}).get("skipped", False)
if skipped:
logger.info(
f"[UniLab Register] 设备注册跳过(内容未变化)"
f" {len(devices_to_register)}{cost_time:.3f}s"
)
elif response.status_code in [200, 201]:
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time:.3f}s")
else: else:
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms") logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
except Exception as e: except Exception as e:
logger.error(f"[UniLab Register] 设备注册异常: {e}") logger.error(f"[UniLab Register] 设备注册异常: {e}")
# 注册资源
if resources_to_register: if resources_to_register:
try: try:
start_time = time.time() start_time = time.time()
response = http_client.resource_registry({"resources": list(resources_to_register.values())}) response = http_client.resource_registry(
{"resources": list(resources_to_register.values())},
tag="resource_registry",
)
cost_time = time.time() - start_time cost_time = time.time() - start_time
if response.status_code in [200, 201]: res_data = response.json() if response.status_code == 200 else {}
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms") skipped = res_data.get("data", {}).get("skipped", False)
if skipped:
logger.info(
f"[UniLab Register] 资源注册跳过(内容未变化)"
f" {len(resources_to_register)}{cost_time:.3f}s"
)
elif response.status_code in [200, 201]:
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time:.3f}s")
else: else:
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms") logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
except Exception as e: except Exception as e:
logger.error(f"[UniLab Register] 资源注册异常: {e}") logger.error(f"[UniLab Register] 资源注册异常: {e}")
logger.info("[UniLab Register] 设备和资源注册完成.")

View File

@@ -1052,7 +1052,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
"result": {}, "result": {},
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k), "schema": lab_registry._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": {},
} }
# 不生成已配置action的动作 # 不生成已配置action的动作
for k, v in enhanced_info["action_methods"].items() for k, v in enhanced_info["action_methods"].items()
@@ -1340,5 +1340,5 @@ def setup_api_routes(app):
# 启动广播任务 # 启动广播任务
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
asyncio.create_task(broadcast_device_status()) asyncio.create_task(broadcast_device_status(), name="web-api-startup-device")
asyncio.create_task(broadcast_status_page_data()) asyncio.create_task(broadcast_status_page_data(), name="web-api-startup-status")

View File

@@ -3,11 +3,13 @@ HTTP客户端模块
提供与远程服务器通信的客户端功能只有host需要用 提供与远程服务器通信的客户端功能只有host需要用
""" """
import gzip
import json import json
import os import os
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from unilabos.utils.tools import fast_dumps as _fast_dumps, fast_dumps_pretty as _fast_dumps_pretty
import requests import requests
from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.resources.resource_tracker import ResourceTreeSet
from unilabos.utils.log import info from unilabos.utils.log import info
@@ -280,22 +282,54 @@ class HTTPClient:
) )
return response return response
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response: def resource_registry(
self, registry_data: Dict[str, Any] | List[Dict[str, Any]], tag: str = "registry",
) -> requests.Response:
""" """
注册资源到服务器 注册资源到服务器,同步保存请求/响应到 unilabos_data
Args: Args:
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}] registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
tag: 保存文件的标签后缀 (如 "device_registry" / "resource_registry")
Returns: Returns:
Response: API响应对象 Response: API响应对象
""" """
# 序列化一次,同时用于保存和发送
json_bytes = _fast_dumps(registry_data)
# 保存请求数据到 unilabos_data
req_path = os.path.join(BasicConfig.working_dir, f"req_{tag}_upload.json")
try:
os.makedirs(BasicConfig.working_dir, exist_ok=True)
with open(req_path, "wb") as f:
f.write(_fast_dumps_pretty(registry_data))
logger.trace(f"注册表请求数据已保存: {req_path}")
except Exception as e:
logger.warning(f"保存注册表请求数据失败: {e}")
compressed_body = gzip.compress(json_bytes)
headers = {
"Authorization": f"Lab {self.auth}",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
}
response = requests.post( response = requests.post(
f"{self.remote_addr}/lab/resource", f"{self.remote_addr}/lab/resource",
json=registry_data, data=compressed_body,
headers={"Authorization": f"Lab {self.auth}"}, headers=headers,
timeout=30, timeout=30,
) )
# 保存响应数据到 unilabos_data
res_path = os.path.join(BasicConfig.working_dir, f"res_{tag}_upload.json")
try:
with open(res_path, "w", encoding="utf-8") as f:
f.write(f"{response.status_code}\n{response.text}")
logger.trace(f"注册表响应数据已保存: {res_path}")
except Exception as e:
logger.warning(f"保存注册表响应数据失败: {e}")
if response.status_code not in [200, 201]: if response.status_code not in [200, 201]:
logger.error(f"注册资源失败: {response.status_code}, {response.text}") logger.error(f"注册资源失败: {response.status_code}, {response.text}")
if response.status_code == 200: if response.status_code == 200:
@@ -343,9 +377,10 @@ class HTTPClient:
edges: List[Dict[str, Any]], edges: List[Dict[str, Any]],
tags: Optional[List[str]] = None, tags: Optional[List[str]] = None,
published: bool = False, published: bool = False,
description: str = "",
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
导入工作流到服务器 导入工作流到服务器,如果 published 为 True则额外发起发布请求
Args: Args:
name: 工作流名称(顶层) name: 工作流名称(顶层)
@@ -355,6 +390,7 @@ class HTTPClient:
edges: 工作流边列表 edges: 工作流边列表
tags: 工作流标签列表,默认为空列表 tags: 工作流标签列表,默认为空列表
published: 是否发布工作流默认为False published: 是否发布工作流默认为False
description: 工作流描述,发布时使用
Returns: Returns:
Dict: API响应数据包含 code 和 data (uuid, name) Dict: API响应数据包含 code 和 data (uuid, name)
@@ -367,7 +403,6 @@ class HTTPClient:
"nodes": nodes, "nodes": nodes,
"edges": edges, "edges": edges,
"tags": tags if tags is not None else [], "tags": tags if tags is not None else [],
"published": published,
}, },
} }
# 保存请求到文件 # 保存请求到文件
@@ -388,11 +423,51 @@ class HTTPClient:
res = response.json() res = response.json()
if "code" in res and res["code"] != 0: if "code" in res and res["code"] != 0:
logger.error(f"导入工作流失败: {response.text}") logger.error(f"导入工作流失败: {response.text}")
return res
# 导入成功后,如果需要发布则额外发起发布请求
if published:
imported_uuid = res.get("data", {}).get("uuid", workflow_uuid)
publish_res = self.workflow_publish(imported_uuid, description)
res["publish_result"] = publish_res
return res return res
else: else:
logger.error(f"导入工作流失败: {response.status_code}, {response.text}") logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
return {"code": response.status_code, "message": response.text} return {"code": response.status_code, "message": response.text}
def workflow_publish(self, workflow_uuid: str, description: str = "") -> Dict[str, Any]:
"""
发布工作流
Args:
workflow_uuid: 工作流UUID
description: 工作流描述
Returns:
Dict: API响应数据
"""
payload = {
"uuid": workflow_uuid,
"description": description,
"published": True,
}
logger.info(f"正在发布工作流: {workflow_uuid}")
response = requests.patch(
f"{self.remote_addr}/lab/workflow/owner",
json=payload,
headers={"Authorization": f"Lab {self.auth}"},
timeout=60,
)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"发布工作流失败: {response.text}")
else:
logger.info(f"工作流发布成功: {workflow_uuid}")
return res
else:
logger.error(f"发布工作流失败: {response.status_code}, {response.text}")
return {"code": response.status_code, "message": response.text}
# 创建默认客户端实例 # 创建默认客户端实例
http_client = HTTPClient() http_client = HTTPClient()

View File

@@ -58,14 +58,14 @@ class JobResultStore:
feedback=feedback or {}, feedback=feedback or {},
timestamp=time.time(), timestamp=time.time(),
) )
logger.trace(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}") logger.debug(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
def get_and_remove(self, job_id: str) -> Optional[JobResult]: def get_and_remove(self, job_id: str) -> Optional[JobResult]:
"""获取并删除任务结果""" """获取并删除任务结果"""
with self._results_lock: with self._results_lock:
result = self._results.pop(job_id, None) result = self._results.pop(job_id, None)
if result: if result:
logger.trace(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}") logger.debug(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
return result return result
def get_result(self, job_id: str) -> Optional[JobResult]: def get_result(self, job_id: str) -> Optional[JobResult]:
@@ -327,6 +327,7 @@ def job_add(req: JobAddReq) -> JobData:
queue_item, queue_item,
action_type=action_type, action_type=action_type,
action_kwargs=action_args, action_kwargs=action_args,
sample_material=req.sample_material,
server_info=server_info, server_info=server_info,
) )

View File

@@ -86,7 +86,7 @@ def setup_server() -> FastAPI:
# 设置页面路由 # 设置页面路由
try: try:
setup_web_pages(pages) setup_web_pages(pages)
info("[Web] 已加载Web UI模块") # info("[Web] 已加载Web UI模块")
except ImportError as e: except ImportError as e:
info(f"[Web] 未找到Web页面模块: {str(e)}") info(f"[Web] 未找到Web页面模块: {str(e)}")
except Exception as e: except Exception as e:
@@ -138,7 +138,7 @@ def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = T
server_thread = threading.Thread(target=server.run, daemon=True, name="uvicorn_server") server_thread = threading.Thread(target=server.run, daemon=True, name="uvicorn_server")
server_thread.start() server_thread.start()
info("[Web] Server started, monitoring for restart requests...") # info("[Web] Server started, monitoring for restart requests...")
# 监控重启标志 # 监控重启标志
import unilabos.app.main as main_module import unilabos.app.main as main_module

View File

@@ -26,6 +26,7 @@ from enum import Enum
from typing_extensions import TypedDict from typing_extensions import TypedDict
from unilabos.app.model import JobAddReq from unilabos.app.model import JobAddReq
from unilabos.resources.resource_tracker import ResourceDictType
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
from unilabos.app.communication import BaseCommunicationClient from unilabos.app.communication import BaseCommunicationClient
@@ -76,6 +77,7 @@ class JobInfo:
start_time: float start_time: float
last_update_time: float = field(default_factory=time.time) last_update_time: float = field(default_factory=time.time)
ready_timeout: Optional[float] = None # READY状态的超时时间 ready_timeout: Optional[float] = None # READY状态的超时时间
always_free: bool = False # 是否为永久闲置动作(不受排队限制)
def update_timestamp(self): def update_timestamp(self):
"""更新最后更新时间""" """更新最后更新时间"""
@@ -127,6 +129,15 @@ class DeviceActionManager:
# 总是将job添加到all_jobs中 # 总是将job添加到all_jobs中
self.all_jobs[job_info.job_id] = job_info self.all_jobs[job_info.job_id] = job_info
# always_free的动作不受排队限制直接设为READY
if job_info.always_free:
job_info.status = JobStatus.READY
job_info.update_timestamp()
job_info.set_ready_timeout(10)
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
logger.trace(f"[DeviceActionManager] Job {job_log} always_free, start immediately")
return True
# 检查是否有正在执行或准备执行的任务 # 检查是否有正在执行或准备执行的任务
if device_key in self.active_jobs: if device_key in self.active_jobs:
# 有正在执行或准备执行的任务,加入队列 # 有正在执行或准备执行的任务,加入队列
@@ -176,11 +187,15 @@ class DeviceActionManager:
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}") logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
return False return False
# 检查设备上是否是这个job # always_free的job不需要检查active_jobs
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id: if not job_info.always_free:
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name) # 检查设备上是否是这个job
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}") if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
return False job_log = format_job_log(
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
)
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
return False
# 开始执行任务将状态从READY转换为STARTED # 开始执行任务将状态从READY转换为STARTED
job_info.status = JobStatus.STARTED job_info.status = JobStatus.STARTED
@@ -203,6 +218,13 @@ class DeviceActionManager:
job_info = self.all_jobs[job_id] job_info = self.all_jobs[job_id]
device_key = job_info.device_action_key device_key = job_info.device_action_key
# always_free的job直接清理不影响队列
if job_info.always_free:
job_info.status = JobStatus.ENDED
job_info.update_timestamp()
del self.all_jobs[job_id]
return None
# 移除活跃任务 # 移除活跃任务
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id: if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
del self.active_jobs[device_key] del self.active_jobs[device_key]
@@ -234,9 +256,14 @@ class DeviceActionManager:
return None return None
def get_active_jobs(self) -> List[JobInfo]: def get_active_jobs(self) -> List[JobInfo]:
"""获取所有正在执行的任务""" """获取所有正在执行的任务(含active_jobs和always_free的STARTED job)"""
with self.lock: with self.lock:
return list(self.active_jobs.values()) jobs = list(self.active_jobs.values())
# 补充 always_free 的 STARTED job(它们不在 active_jobs 中)
for job in self.all_jobs.values():
if job.always_free and job.status == JobStatus.STARTED and job not in jobs:
jobs.append(job)
return jobs
def get_queued_jobs(self) -> List[JobInfo]: def get_queued_jobs(self) -> List[JobInfo]:
"""获取所有排队中的任务""" """获取所有排队中的任务"""
@@ -261,6 +288,14 @@ class DeviceActionManager:
job_info = self.all_jobs[job_id] job_info = self.all_jobs[job_id]
device_key = job_info.device_action_key device_key = job_info.device_action_key
# always_free的job直接清理
if job_info.always_free:
job_info.status = JobStatus.ENDED
del self.all_jobs[job_id]
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
logger.trace(f"[DeviceActionManager] Always-free job {job_log} cancelled")
return True
# 如果是正在执行的任务 # 如果是正在执行的任务
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id: if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
# 清理active job状态 # 清理active job状态
@@ -334,13 +369,18 @@ class DeviceActionManager:
timeout_jobs = [] timeout_jobs = []
with self.lock: with self.lock:
# 统计READY状态的任务数量 # 收集所有需要检查的 READY 任务(active_jobs + always_free READY jobs)
ready_jobs_count = sum(1 for job in self.active_jobs.values() if job.status == JobStatus.READY) ready_candidates = list(self.active_jobs.values())
for job in self.all_jobs.values():
if job.always_free and job.status == JobStatus.READY and job not in ready_candidates:
ready_candidates.append(job)
ready_jobs_count = sum(1 for job in ready_candidates if job.status == JobStatus.READY)
if ready_jobs_count > 0: if ready_jobs_count > 0:
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501 logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
# 找到所有超时的READY任务只检测不处理 # 找到所有超时的READY任务只检测不处理
for job_info in self.active_jobs.values(): for job_info in ready_candidates:
if job_info.is_ready_timeout(): if job_info.is_ready_timeout():
timeout_jobs.append(job_info) timeout_jobs.append(job_info)
job_log = format_job_log( job_log = format_job_log(
@@ -369,6 +409,7 @@ class MessageProcessor:
# 线程控制 # 线程控制
self.is_running = False self.is_running = False
self.thread = None self.thread = None
self._loop = None # asyncio event loop引用用于外部关闭websocket
self.reconnect_count = 0 self.reconnect_count = 0
logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}") logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}")
@@ -395,22 +436,31 @@ class MessageProcessor:
def stop(self) -> None: def stop(self) -> None:
"""停止消息处理线程""" """停止消息处理线程"""
self.is_running = False self.is_running = False
# 主动关闭websocket以快速中断消息接收循环
ws = self.websocket
loop = self._loop
if ws and loop and loop.is_running():
try:
asyncio.run_coroutine_threadsafe(ws.close(), loop)
except Exception:
pass
if self.thread and self.thread.is_alive(): if self.thread and self.thread.is_alive():
self.thread.join(timeout=2) self.thread.join(timeout=2)
logger.info("[MessageProcessor] Stopped") logger.info("[MessageProcessor] Stopped")
def _run(self): def _run(self):
"""运行消息处理主循环""" """运行消息处理主循环"""
loop = asyncio.new_event_loop() self._loop = asyncio.new_event_loop()
try: try:
asyncio.set_event_loop(loop) asyncio.set_event_loop(self._loop)
loop.run_until_complete(self._connection_handler()) self._loop.run_until_complete(self._connection_handler())
except Exception as e: except Exception as e:
logger.error(f"[MessageProcessor] Thread error: {str(e)}") logger.error(f"[MessageProcessor] Thread error: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
finally: finally:
if loop: if self._loop:
loop.close() self._loop.close()
self._loop = None
async def _connection_handler(self): async def _connection_handler(self):
"""处理WebSocket连接和重连逻辑""" """处理WebSocket连接和重连逻辑"""
@@ -427,8 +477,10 @@ class MessageProcessor:
async with websockets.connect( async with websockets.connect(
self.websocket_url, self.websocket_url,
ssl=ssl_context, ssl=ssl_context,
open_timeout=20,
ping_interval=WSConfig.ping_interval, ping_interval=WSConfig.ping_interval,
ping_timeout=10, ping_timeout=10,
close_timeout=5,
additional_headers={ additional_headers={
"Authorization": f"Lab {BasicConfig.auth_secret()}", "Authorization": f"Lab {BasicConfig.auth_secret()}",
"EdgeSession": f"{self.session_id}", "EdgeSession": f"{self.session_id}",
@@ -439,81 +491,94 @@ class MessageProcessor:
self.connected = True self.connected = True
self.reconnect_count = 0 self.reconnect_count = 0
logger.trace(f"[MessageProcessor] Connected to {self.websocket_url}") logger.info(f"[MessageProcessor] 已连接到 {self.websocket_url}")
# 启动发送协程 # 启动发送协程
send_task = asyncio.create_task(self._send_handler()) send_task = asyncio.create_task(self._send_handler(), name="websocket-send_task")
# 每次连接(含重连)后重新向服务端注册,
# 否则服务端不知道客户端已上线,不会推送消息。
if self.websocket_client:
self.websocket_client.publish_host_ready()
try: try:
# 接收消息循环 # 接收消息循环
await self._message_handler() await self._message_handler()
finally: finally:
# 必须在 async with __aexit__ 之前停止 send_task
# 否则 send_task 会在关闭握手期间继续发送数据,
# 干扰 websockets 库的内部清理,导致 task 泄漏。
self.connected = False
send_task.cancel() send_task.cancel()
try: try:
await send_task await send_task
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
self.connected = False
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
logger.warning("[MessageProcessor] Connection closed") logger.warning("[MessageProcessor] 与服务端连接中断")
self.connected = False except TimeoutError:
logger.warning(
f"[MessageProcessor] 与服务端连接通信超时 (已尝试 {self.reconnect_count + 1} 次),请检查您的网络状况"
)
except websockets.exceptions.InvalidStatus as e:
logger.warning(
f"[MessageProcessor] 收到服务端注册码 {e.response.status_code}, 上一进程可能还未退出"
)
except Exception as e: except Exception as e:
logger.error(f"[MessageProcessor] Connection error: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
self.connected = False logger.error(f"[MessageProcessor] 尝试重连时出错 {str(e)}")
finally: finally:
self.connected = False
self.websocket = None self.websocket = None
# 重连逻辑 # 重连逻辑
if self.is_running and self.reconnect_count < WSConfig.max_reconnect_attempts: if not self.is_running:
break
if self.reconnect_count < WSConfig.max_reconnect_attempts:
self.reconnect_count += 1 self.reconnect_count += 1
backoff = WSConfig.reconnect_interval
logger.info( logger.info(
f"[MessageProcessor] Reconnecting in {WSConfig.reconnect_interval}s " f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
) )
await asyncio.sleep(WSConfig.reconnect_interval) await asyncio.sleep(backoff)
elif self.reconnect_count >= WSConfig.max_reconnect_attempts: else:
logger.error("[MessageProcessor] Max reconnection attempts reached") logger.error("[MessageProcessor] Max reconnection attempts reached")
break break
else:
self.reconnect_count -= 1
async def _message_handler(self): async def _message_handler(self):
"""处理接收到的消息""" """处理接收到的消息
ConnectionClosed 不在此处捕获,让其向上传播到 _connection_handler
以便 async with websockets.connect() 的 __aexit__ 能感知连接已断,
正确清理内部 task避免 task 泄漏。
"""
if not self.websocket: if not self.websocket:
logger.error("[MessageProcessor] WebSocket connection is None") logger.error("[MessageProcessor] WebSocket connection is None")
return return
try: async for message in self.websocket:
async for message in self.websocket: try:
try: data = json.loads(message)
data = json.loads(message) message_type = data.get("action", "")
message_type = data.get("action", "") message_data = data.get("data")
message_data = data.get("data") if self.session_id and self.session_id == data.get("edge_session"):
if self.session_id and self.session_id == data.get("edge_session"): await self._process_message(message_type, message_data)
await self._process_message(message_type, message_data) else:
if message_type.endswith("_material"):
logger.trace(
f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}"
)
logger.debug(
f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}"
)
else: else:
if message_type.endswith("_material"): await self._process_message(message_type, message_data)
logger.trace( except json.JSONDecodeError:
f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}" logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
) except Exception as e:
logger.debug( logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}" logger.error(traceback.format_exc())
)
else:
await self._process_message(message_type, message_data)
except json.JSONDecodeError:
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
except Exception as e:
logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
logger.error(traceback.format_exc())
except websockets.exceptions.ConnectionClosed:
logger.info("[MessageProcessor] Message handler stopped - connection closed")
except Exception as e:
logger.error(f"[MessageProcessor] Message handler error: {str(e)}")
logger.error(traceback.format_exc())
async def _send_handler(self): async def _send_handler(self):
"""处理发送队列中的消息""" """处理发送队列中的消息"""
@@ -545,7 +610,7 @@ class MessageProcessor:
try: try:
message_str = json.dumps(msg, ensure_ascii=False) message_str = json.dumps(msg, ensure_ascii=False)
await self.websocket.send(message_str) await self.websocket.send(message_str)
logger.trace(f"[MessageProcessor] Message sent: {msg.get('action', 'unknown')}") # type: ignore # noqa: E501 # logger.trace(f"[MessageProcessor] Message sent: {msg.get('action', 'unknown')}") # type: ignore # noqa: E501
except Exception as e: except Exception as e:
logger.error(f"[MessageProcessor] Failed to send message: {str(e)}") logger.error(f"[MessageProcessor] Failed to send message: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@@ -562,6 +627,7 @@ class MessageProcessor:
except asyncio.CancelledError: except asyncio.CancelledError:
logger.debug("[MessageProcessor] Send handler cancelled") logger.debug("[MessageProcessor] Send handler cancelled")
raise
except Exception as e: except Exception as e:
logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}") logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@@ -593,6 +659,10 @@ class MessageProcessor:
# elif message_type == "session_id": # elif message_type == "session_id":
# self.session_id = message_data.get("session_id") # self.session_id = message_data.get("session_id")
# logger.info(f"[MessageProcessor] Session ID: {self.session_id}") # logger.info(f"[MessageProcessor] Session ID: {self.session_id}")
elif message_type == "add_device":
await self._handle_device_manage(message_data, "add")
elif message_type == "remove_device":
await self._handle_device_manage(message_data, "remove")
elif message_type == "request_restart": elif message_type == "request_restart":
await self._handle_request_restart(message_data) await self._handle_request_restart(message_data)
else: else:
@@ -608,6 +678,24 @@ class MessageProcessor:
if host_node: if host_node:
host_node.handle_pong_response(pong_data) host_node.handle_pong_response(pong_data)
def _check_action_always_free(self, device_id: str, action_name: str) -> bool:
"""检查该action是否标记为always_free通过HostNode统一的_action_value_mappings查找"""
try:
host_node = HostNode.get_instance(0)
if not host_node:
return False
# noinspection PyProtectedMember
action_mappings = host_node._action_value_mappings.get(device_id)
if not action_mappings:
return False
# 尝试直接匹配或 auto- 前缀匹配
for key in [action_name, f"auto-{action_name}"]:
if key in action_mappings:
return action_mappings[key].get("always_free", False)
return False
except Exception:
return False
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", "")
@@ -622,6 +710,9 @@ class MessageProcessor:
device_action_key = f"/devices/{device_id}/{action_name}" device_action_key = f"/devices/{device_id}/{action_name}"
# 检查action是否为always_free
action_always_free = self._check_action_always_free(device_id, action_name)
# 创建任务信息 # 创建任务信息
job_info = JobInfo( job_info = JobInfo(
job_id=job_id, job_id=job_id,
@@ -631,6 +722,7 @@ class MessageProcessor:
device_action_key=device_action_key, device_action_key=device_action_key,
status=JobStatus.QUEUE, status=JobStatus.QUEUE,
start_time=time.time(), start_time=time.time(),
always_free=action_always_free,
) )
# 添加到设备管理器 # 添加到设备管理器
@@ -657,9 +749,37 @@ class MessageProcessor:
async def _handle_job_start(self, data: Dict[str, Any]): async def _handle_job_start(self, data: Dict[str, Any]):
"""处理job_start消息""" """处理job_start消息"""
try: try:
if not data.get("sample_material"):
data["sample_material"] = {}
req = JobAddReq(**data) req = JobAddReq(**data)
job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action) job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action)
# 服务端对always_free动作可能跳过query_action_state直接发job_start
# 此时job尚未注册需要自动补注册
existing_job = self.device_manager.get_job_info(req.job_id)
if not existing_job:
action_name = req.action
device_action_key = f"/devices/{req.device_id}/{action_name}"
action_always_free = self._check_action_always_free(req.device_id, action_name)
if action_always_free:
job_info = JobInfo(
job_id=req.job_id,
task_id=req.task_id,
device_id=req.device_id,
action_name=action_name,
device_action_key=device_action_key,
status=JobStatus.QUEUE,
start_time=time.time(),
always_free=True,
)
self.device_manager.add_queue_request(job_info)
logger.info(f"[MessageProcessor] Job {job_log} always_free, auto-registered from direct job_start")
else:
logger.error(f"[MessageProcessor] Job {job_log} not registered (missing query_action_state)")
return
success = self.device_manager.start_job(req.job_id) success = self.device_manager.start_job(req.job_id)
if not success: if not success:
logger.error(f"[MessageProcessor] Failed to start job {job_log}") logger.error(f"[MessageProcessor] Failed to start job {job_log}")
@@ -688,6 +808,7 @@ class MessageProcessor:
queue_item, queue_item,
action_type=req.action_type, action_type=req.action_type,
action_kwargs=req.action_args, action_kwargs=req.action_args,
sample_material=req.sample_material,
server_info=req.server_info, server_info=req.server_info,
) )
@@ -904,6 +1025,37 @@ class MessageProcessor:
) )
thread.start() thread.start()
async def _handle_device_manage(self, device_list: list[ResourceDictType], action: str):
"""Handle add_device / remove_device from LabGo server."""
if not device_list:
return
for item in device_list:
target_node_id = item.get("target_node_id", "host_node")
def _notify(target_id: str, act: str, cfg: ResourceDictType):
try:
host_node = HostNode.get_instance(timeout=5)
if not host_node:
logger.error(f"[DeviceManage] HostNode not available for {act}_device")
return
success = host_node.notify_device_manage(target_id, act, cfg)
if success:
logger.info(f"[DeviceManage] {act}_device completed on {target_id}")
else:
logger.warning(f"[DeviceManage] {act}_device failed on {target_id}")
except Exception as e:
logger.error(f"[DeviceManage] Error in {act}_device: {e}")
logger.error(traceback.format_exc())
thread = threading.Thread(
target=_notify,
args=(target_node_id, action, item),
daemon=True,
name=f"DeviceManage-{action}-{item.get('id', '')}",
)
thread.start()
async def _handle_request_restart(self, data: Dict[str, Any]): async def _handle_request_restart(self, data: Dict[str, Any]):
""" """
处理重启请求 处理重启请求
@@ -915,10 +1067,9 @@ class MessageProcessor:
logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s") logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s")
# 发送确认消息 # 发送确认消息
if self.websocket_client: self.send_message(
await self.websocket_client.send_message( {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}}
{"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} )
)
# 设置全局重启标志 # 设置全局重启标志
import unilabos.app.main as main_module import unilabos.app.main as main_module
@@ -1020,6 +1171,7 @@ class QueueProcessor:
def stop(self) -> None: def stop(self) -> None:
"""停止队列处理线程""" """停止队列处理线程"""
self.is_running = False self.is_running = False
self.queue_update_event.set() # 立即唤醒等待中的线程
if self.thread and self.thread.is_alive(): if self.thread and self.thread.is_alive():
self.thread.join(timeout=2) self.thread.join(timeout=2)
logger.info("[QueueProcessor] Stopped") logger.info("[QueueProcessor] Stopped")
@@ -1120,6 +1272,11 @@ class QueueProcessor:
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs") logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
for job_info in queued_jobs: for job_info in queued_jobs:
# 快照可能已过期:在遍历过程中 end_job() 可能已将此 job 移至 READY
# 此时不应再发送 busy/need_more否则会覆盖已发出的 free=True 通知
if job_info.status != JobStatus.QUEUE:
continue
message = { message = {
"action": "report_action_state", "action": "report_action_state",
"data": { "data": {
@@ -1268,8 +1425,8 @@ class WebSocketClient(BaseCommunicationClient):
message = {"action": "normal_exit", "data": {"session_id": session_id}} message = {"action": "normal_exit", "data": {"session_id": session_id}}
self.message_processor.send_message(message) self.message_processor.send_message(message)
logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}") logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}")
# 给一点时间让消息发送出去 # send_handler 每100ms检查一次队列等300ms足以让消息发
time.sleep(1) time.sleep(0.3)
except Exception as e: except Exception as e:
logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}") logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}")
@@ -1301,7 +1458,7 @@ class WebSocketClient(BaseCommunicationClient):
}, },
} }
self.message_processor.send_message(message) self.message_processor.send_message(message)
logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}") # logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
def publish_job_status( def publish_job_status(
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None

View File

@@ -95,8 +95,29 @@ def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
return total_volume return total_volume
def is_integrated_pump(node_name): def is_integrated_pump(node_class: str, node_name: str = "") -> bool:
return "pump" in node_name and "valve" in node_name """
判断是否为泵阀一体设备
"""
class_lower = (node_class or "").lower()
name_lower = (node_name or "").lower()
if "pump" not in class_lower and "pump" not in name_lower:
return False
integrated_markers = [
"valve",
"pump_valve",
"pumpvalve",
"integrated",
"transfer_pump",
]
for marker in integrated_markers:
if marker in class_lower or marker in name_lower:
return True
return False
def find_connected_pump(G, valve_node): def find_connected_pump(G, valve_node):
@@ -186,7 +207,9 @@ def build_pump_valve_maps(G, pump_backbone):
debug_print(f"🔧 过滤后的骨架: {filtered_backbone}") debug_print(f"🔧 过滤后的骨架: {filtered_backbone}")
for node in filtered_backbone: for node in filtered_backbone:
if is_integrated_pump(G.nodes[node]["class"]): node_data = G.nodes.get(node, {})
node_class = node_data.get("class", "") or ""
if is_integrated_pump(node_class, node):
pumps_from_node[node] = node pumps_from_node[node] = node
valve_from_node[node] = node valve_from_node[node] = node
debug_print(f" - 集成泵-阀: {node}") debug_print(f" - 集成泵-阀: {node}")

View File

@@ -23,6 +23,8 @@ class BasicConfig:
disable_browser = False # 禁止浏览器自动打开 disable_browser = False # 禁止浏览器自动打开
port = 8002 # 本地HTTP服务 port = 8002 # 本地HTTP服务
check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性 check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性
test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果
extra_resource = False # 是否加载lab_开头的额外资源
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG" log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
@@ -39,7 +41,7 @@ class BasicConfig:
class WSConfig: class WSConfig:
reconnect_interval = 5 # 重连间隔(秒) reconnect_interval = 5 # 重连间隔(秒)
max_reconnect_attempts = 999 # 最大重连次数 max_reconnect_attempts = 999 # 最大重连次数
ping_interval = 30 # ping间隔 ping_interval = 20 # ping间隔
# HTTP配置 # HTTP配置
@@ -145,5 +147,5 @@ def load_config(config_path=None):
traceback.print_exc() traceback.print_exc()
exit(1) exit(1)
else: else:
config_path = os.path.join(os.path.dirname(__file__), "local_config.py") config_path = os.path.join(os.path.dirname(__file__), "example_config.py")
load_config(config_path) load_config(config_path)

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ class Base(ABC):
self._type = typ self._type = typ
self._data_type = data_type self._data_type = data_type
self._node: Optional[Node] = None self._node: Optional[Node] = None
def _get_node(self) -> Node: def _get_node(self) -> Node:
if self._node is None: if self._node is None:
try: try:
@@ -66,7 +66,7 @@ class Base(ABC):
# 直接以字符串形式处理 # 直接以字符串形式处理
if isinstance(nid, str): if isinstance(nid, str):
nid = nid.strip() nid = nid.strip()
# 处理包含类名的格式,如 'StringNodeId(ns=4;s=...)' 或 'NumericNodeId(ns=2;i=...)' # 处理包含类名的格式,如 'StringNodeId(ns=4;s=...)' 或 'NumericNodeId(ns=2;i=...)'
# 提取括号内的内容 # 提取括号内的内容
match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid) match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid)
@@ -116,16 +116,16 @@ class Base(ABC):
def read(self) -> Tuple[Any, bool]: def read(self) -> Tuple[Any, bool]:
"""读取节点值,返回(值, 是否出错)""" """读取节点值,返回(值, 是否出错)"""
pass pass
@abstractmethod @abstractmethod
def write(self, value: Any) -> bool: def write(self, value: Any) -> bool:
"""写入节点值,返回是否出错""" """写入节点值,返回是否出错"""
pass pass
@property @property
def type(self) -> NodeType: def type(self) -> NodeType:
return self._type return self._type
@property @property
def node_id(self) -> str: def node_id(self) -> str:
return self._node_id return self._node_id
@@ -210,15 +210,15 @@ class Method(Base):
super().__init__(client, name, node_id, NodeType.METHOD, data_type) super().__init__(client, name, node_id, NodeType.METHOD, data_type)
self._parent_node_id = parent_node_id self._parent_node_id = parent_node_id
self._parent_node = None self._parent_node = None
def _get_parent_node(self) -> Node: def _get_parent_node(self) -> Node:
if self._parent_node is None: if self._parent_node is None:
try: try:
# 处理父节点ID使用与_get_node相同的解析逻辑 # 处理父节点ID使用与_get_node相同的解析逻辑
import re import re
nid = self._parent_node_id nid = self._parent_node_id
# 如果已经是 NodeId 对象,直接使用 # 如果已经是 NodeId 对象,直接使用
try: try:
from opcua.ua import NodeId as UaNodeId from opcua.ua import NodeId as UaNodeId
@@ -227,16 +227,16 @@ class Method(Base):
return self._parent_node return self._parent_node
except Exception: except Exception:
pass pass
# 字符串处理 # 字符串处理
if isinstance(nid, str): if isinstance(nid, str):
nid = nid.strip() nid = nid.strip()
# 处理包含类名的格式 # 处理包含类名的格式
match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid) match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid)
if match_wrapped: if match_wrapped:
nid = match_wrapped.group(2).strip() nid = match_wrapped.group(2).strip()
# 常见短格式 # 常见短格式
if re.match(r'^ns=\d+;[is]=', nid): if re.match(r'^ns=\d+;[is]=', nid):
self._parent_node = self._client.get_node(nid) self._parent_node = self._client.get_node(nid)
@@ -271,7 +271,7 @@ class Method(Base):
def write(self, value: Any) -> bool: def write(self, value: Any) -> bool:
"""方法节点不支持写入操作""" """方法节点不支持写入操作"""
return True return True
def call(self, *args) -> Tuple[Any, bool]: def call(self, *args) -> Tuple[Any, bool]:
"""调用方法,返回(返回值, 是否出错)""" """调用方法,返回(返回值, 是否出错)"""
try: try:
@@ -285,7 +285,7 @@ class Method(Base):
class Object(Base): class Object(Base):
def __init__(self, client: Client, name: str, node_id: str): def __init__(self, client: Client, name: str, node_id: str):
super().__init__(client, name, node_id, NodeType.OBJECT, None) super().__init__(client, name, node_id, NodeType.OBJECT, None)
def read(self) -> Tuple[Any, bool]: def read(self) -> Tuple[Any, bool]:
"""对象节点不支持直接读取操作""" """对象节点不支持直接读取操作"""
return None, True return None, True
@@ -293,7 +293,7 @@ class Object(Base):
def write(self, value: Any) -> bool: def write(self, value: Any) -> bool:
"""对象节点不支持直接写入操作""" """对象节点不支持直接写入操作"""
return True return True
def get_children(self) -> Tuple[List[Node], bool]: def get_children(self) -> Tuple[List[Node], bool]:
"""获取子节点列表,返回(子节点列表, 是否出错)""" """获取子节点列表,返回(子节点列表, 是否出错)"""
try: try:
@@ -301,4 +301,4 @@ class Object(Base):
return children, False return children, False
except Exception as e: except Exception as e:
print(f"获取对象 {self._name} 的子节点失败: {e}") print(f"获取对象 {self._name} 的子节点失败: {e}")
return [], True return [], True

View File

@@ -1,4 +1,3 @@
from abc import abstractmethod from abc import abstractmethod
from functools import wraps from functools import wraps
import inspect import inspect

View File

@@ -201,17 +201,42 @@ class ResourceVisualization:
self.moveit_controllers_yaml['moveit_simple_controller_manager'][f"{name}_{controller_name}"] = moveit_dict['moveit_simple_controller_manager'][controller_name] self.moveit_controllers_yaml['moveit_simple_controller_manager'][f"{name}_{controller_name}"] = moveit_dict['moveit_simple_controller_manager'][controller_name]
@staticmethod
def _ensure_ros2_env() -> dict:
"""确保 ROS2 环境变量正确设置,返回可用于子进程的 env dict"""
import sys
env = dict(os.environ)
conda_prefix = os.path.dirname(os.path.dirname(sys.executable))
if "AMENT_PREFIX_PATH" not in env or not env["AMENT_PREFIX_PATH"].strip():
candidate = os.pathsep.join([conda_prefix, os.path.join(conda_prefix, "Library")])
env["AMENT_PREFIX_PATH"] = candidate
os.environ["AMENT_PREFIX_PATH"] = candidate
extra_bin_dirs = [
os.path.join(conda_prefix, "Library", "bin"),
os.path.join(conda_prefix, "Library", "lib"),
os.path.join(conda_prefix, "Scripts"),
conda_prefix,
]
current_path = env.get("PATH", "")
for d in extra_bin_dirs:
if d not in current_path:
current_path = d + os.pathsep + current_path
env["PATH"] = current_path
os.environ["PATH"] = current_path
return env
def create_launch_description(self) -> LaunchDescription: def create_launch_description(self) -> LaunchDescription:
""" """
创建launch描述包含robot_state_publisher和move_group节点 创建launch描述包含robot_state_publisher和move_group节点
Args:
urdf_str: URDF文本
Returns: Returns:
LaunchDescription: launch描述对象 LaunchDescription: launch描述对象
""" """
# 检查ROS 2环境变量 launch_env = self._ensure_ros2_env()
if "AMENT_PREFIX_PATH" not in os.environ: if "AMENT_PREFIX_PATH" not in os.environ:
raise OSError( raise OSError(
"ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n" "ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n"
@@ -290,7 +315,7 @@ class ResourceVisualization:
{"robot_description": robot_description}, {"robot_description": robot_description},
ros2_controllers, ros2_controllers,
], ],
env=dict(os.environ) env=launch_env,
) )
) )
for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']: for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']:
@@ -300,7 +325,7 @@ class ResourceVisualization:
executable="spawner", executable="spawner",
arguments=[f"{controller}", "--controller-manager", f"controller_manager"], arguments=[f"{controller}", "--controller-manager", f"controller_manager"],
output="screen", output="screen",
env=dict(os.environ) env=launch_env,
) )
) )
controllers.append( controllers.append(
@@ -309,7 +334,7 @@ class ResourceVisualization:
executable="spawner", executable="spawner",
arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"], arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"],
output="screen", output="screen",
env=dict(os.environ) env=launch_env,
) )
) )
for i in controllers: for i in controllers:
@@ -317,7 +342,6 @@ class ResourceVisualization:
else: else:
ros2_controllers = None ros2_controllers = None
# 创建robot_state_publisher节点
robot_state_publisher = nd( robot_state_publisher = nd(
package='robot_state_publisher', package='robot_state_publisher',
executable='robot_state_publisher', executable='robot_state_publisher',
@@ -327,9 +351,8 @@ class ResourceVisualization:
'robot_description': robot_description, 'robot_description': robot_description,
'use_sim_time': False 'use_sim_time': False
}, },
# kinematics_dict
], ],
env=dict(os.environ) env=launch_env,
) )
@@ -361,7 +384,7 @@ class ResourceVisualization:
executable='move_group', executable='move_group',
output='screen', output='screen',
parameters=moveit_params, parameters=moveit_params,
env=dict(os.environ) env=launch_env,
) )
@@ -379,13 +402,11 @@ class ResourceVisualization:
arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"], arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"],
output='screen', output='screen',
parameters=[ parameters=[
{'robot_description_kinematics': kinematics_dict, {'robot_description_kinematics': kinematics_dict},
},
robot_description_planning, robot_description_planning,
planning_pipelines, planning_pipelines,
], ],
env=dict(os.environ) env=launch_env,
) )
self.launch_description.add_action(rviz_node) self.launch_description.add_action(rviz_node)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -59,6 +59,7 @@ class UniLiquidHandlerRvizBackend(LiquidHandlerBackend):
self.total_height = total_height self.total_height = total_height
self.joint_config = kwargs.get("joint_config", None) self.joint_config = kwargs.get("joint_config", None)
self.lh_device_id = kwargs.get("lh_device_id", "lh_joint_publisher") self.lh_device_id = kwargs.get("lh_device_id", "lh_joint_publisher")
self.simulate_rviz = kwargs.get("simulate_rviz", False)
if not rclpy.ok(): if not rclpy.ok():
rclpy.init() rclpy.init()
self.joint_state_publisher = None self.joint_state_publisher = None
@@ -69,7 +70,7 @@ class UniLiquidHandlerRvizBackend(LiquidHandlerBackend):
self.joint_state_publisher = LiquidHandlerJointPublisher( self.joint_state_publisher = LiquidHandlerJointPublisher(
joint_config=self.joint_config, joint_config=self.joint_config,
lh_device_id=self.lh_device_id, lh_device_id=self.lh_device_id,
simulate_rviz=True) simulate_rviz=self.simulate_rviz)
# 启动ROS executor # 启动ROS executor
self.executor = rclpy.executors.MultiThreadedExecutor() self.executor = rclpy.executors.MultiThreadedExecutor()

View File

@@ -19,10 +19,11 @@ from rclpy.node import Node
import re import re
class LiquidHandlerJointPublisher(BaseROS2DeviceNode): class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", **kwargs): def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", registry_name: str = "lh_joint_publisher", **kwargs):
super().__init__( super().__init__(
driver_instance=self, driver_instance=self,
device_id=device_id, device_id=device_id,
registry_name=registry_name,
status_types={}, status_types={},
action_value_mappings={}, action_value_mappings={},
hardware_interface={}, hardware_interface={},

View File

@@ -42,6 +42,7 @@ class LiquidHandlerJointPublisher(Node):
while self.resource_action is None: while self.resource_action is None:
self.resource_action = self.check_tf_update_actions() self.resource_action = self.check_tf_update_actions()
time.sleep(1) time.sleep(1)
self.get_logger().info(f'Waiting for TfUpdate server: {self.resource_action}')
self.resource_action_client = ActionClient(self, SendCmd, self.resource_action) self.resource_action_client = ActionClient(self, SendCmd, self.resource_action)
while not self.resource_action_client.wait_for_server(timeout_sec=1.0): while not self.resource_action_client.wait_for_server(timeout_sec=1.0):

View File

@@ -0,0 +1,88 @@
"""虚拟样品演示设备 — 用于前端 sample tracking 功能的极简 demo"""
import asyncio
import logging
import random
import time
from typing import Any, Dict, List, Optional
class VirtualSampleDemo:
"""虚拟样品追踪演示设备,提供两种典型返回模式:
- measure_samples: 等长输入输出 (前端按 index 自动对齐)
- split_and_measure: 输出比输入长,附带 samples 列标注归属
"""
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
if device_id is None and "id" in kwargs:
device_id = kwargs.pop("id")
if config is None and "config" in kwargs:
config = kwargs.pop("config")
self.device_id = device_id or "unknown_sample_demo"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualSampleDemo.{self.device_id}")
self.data: Dict[str, Any] = {"status": "Idle"}
# ------------------------------------------------------------------
# Action 1: 等长输入输出,无 samples 列
# ------------------------------------------------------------------
async def measure_samples(self, concentrations: List[float]) -> Dict[str, Any]:
"""模拟光度测量。absorbance = concentration * 0.05 + noise
入参和出参 list 长度相等,前端按 index 自动对齐。
"""
self.logger.info(f"measure_samples: concentrations={concentrations}")
absorbance = [round(c * 0.05 + random.gauss(0, 0.005), 4) for c in concentrations]
return {"concentrations": concentrations, "absorbance": absorbance}
# ------------------------------------------------------------------
# Action 2: 输出比输入长,带 samples 列
# ------------------------------------------------------------------
async def split_and_measure(self, volumes: List[float], split_count: int = 3) -> Dict[str, Any]:
"""将每个样品均分为 split_count 份后逐份测量。
返回的 list 长度 = len(volumes) * split_count
附带 samples 列标注每行属于第几个输入样品 (0-based index)。
"""
self.logger.info(f"split_and_measure: volumes={volumes}, split_count={split_count}")
out_volumes: List[float] = []
readings: List[float] = []
samples: List[int] = []
for idx, vol in enumerate(volumes):
split_vol = round(vol / split_count, 2)
for _ in range(split_count):
out_volumes.append(split_vol)
readings.append(round(random.uniform(0.1, 1.0), 4))
samples.append(idx)
return {"volumes": out_volumes, "readings": readings, "unilabos_samples": samples}
# ------------------------------------------------------------------
# Action 3: 入参和出参都带 samples 列(不等长)
# ------------------------------------------------------------------
async def analyze_readings(self, readings: List[float], samples: List[int]) -> Dict[str, Any]:
"""对 split_and_measure 的输出做二次分析。
入参 readings/samples 长度相同但 > 原始样品数,
出参同样带 samples 列,长度与入参一致。
"""
self.logger.info(f"analyze_readings: readings={readings}, samples={samples}")
scores: List[float] = []
passed: List[bool] = []
threshold = 0.4
for r in readings:
score = round(r * 100 + random.gauss(0, 2), 2)
scores.append(score)
passed.append(r >= threshold)
return {"scores": scores, "passed": passed, "unilabos_samples": samples}
# ------------------------------------------------------------------
# 状态属性
# ------------------------------------------------------------------
@property
def status(self) -> str:
return self.data.get("status", "Idle")

View File

@@ -15,35 +15,35 @@ class VirtualPumpMode(Enum):
class VirtualTransferPump: class VirtualTransferPump:
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰""" """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
_ros_node: BaseROS2DeviceNode _ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = None, config: dict = None, **kwargs): def __init__(self, device_id: str = None, config: dict = None, **kwargs):
""" """
初始化虚拟转移泵 初始化虚拟转移泵
Args: Args:
device_id: 设备ID device_id: 设备ID
config: 配置字典包含max_volume, port等参数 config: 配置字典包含max_volume, port等参数
**kwargs: 其他参数,确保兼容性 **kwargs: 其他参数,确保兼容性
""" """
self.device_id = device_id or "virtual_transfer_pump" self.device_id = device_id or "virtual_transfer_pump"
# 从config或kwargs中获取参数确保类型正确 # 从config或kwargs中获取参数确保类型正确
if config: if config:
self.max_volume = float(config.get('max_volume', 25.0)) self.max_volume = float(config.get("max_volume", 25.0))
self.port = config.get('port', 'VIRTUAL') self.port = config.get("port", "VIRTUAL")
else: else:
self.max_volume = float(kwargs.get('max_volume', 25.0)) self.max_volume = float(kwargs.get("max_volume", 25.0))
self.port = kwargs.get('port', 'VIRTUAL') self.port = kwargs.get("port", "VIRTUAL")
self._transfer_rate = float(kwargs.get('transfer_rate', 0)) self._transfer_rate = float(kwargs.get("transfer_rate", 0))
self.mode = kwargs.get('mode', VirtualPumpMode.Normal) self.mode = kwargs.get("mode", VirtualPumpMode.Normal)
# 状态变量 - 确保都是正确类型 # 状态变量 - 确保都是正确类型
self._status = "Idle" self._status = "Idle"
self._position = 0.0 # float self._position = 0.0 # float
self._max_velocity = 5.0 # float self._max_velocity = 5.0 # float
self._current_volume = 0.0 # float self._current_volume = 0.0 # float
# 🚀 新增:快速模式设置 - 大幅缩短执行时间 # 🚀 新增:快速模式设置 - 大幅缩短执行时间
@@ -52,14 +52,16 @@ class VirtualTransferPump:
self._fast_dispense_time = 1.0 # 快速喷射时间(秒) self._fast_dispense_time = 1.0 # 快速喷射时间(秒)
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}") self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨") print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨")
print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s") print(
f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s"
)
print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}") print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
def post_init(self, ros_node: BaseROS2DeviceNode): def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""初始化虚拟泵 🚀""" """初始化虚拟泵 🚀"""
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id}") self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id}")
@@ -68,33 +70,33 @@ class VirtualTransferPump:
self._current_volume = 0.0 self._current_volume = 0.0
self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰") self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰")
return True return True
async def cleanup(self) -> bool: async def cleanup(self) -> bool:
"""清理虚拟泵 🧹""" """清理虚拟泵 🧹"""
self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚") self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚")
self._status = "Idle" self._status = "Idle"
self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤") self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤")
return True return True
# 基本属性 # 基本属性
@property @property
def status(self) -> str: def status(self) -> str:
return self._status return self._status
@property @property
def position(self) -> float: def position(self) -> float:
"""当前柱塞位置 (ml) 📍""" """当前柱塞位置 (ml) 📍"""
return self._position return self._position
@property @property
def current_volume(self) -> float: def current_volume(self) -> float:
"""当前注射器中的体积 (ml) 💧""" """当前注射器中的体积 (ml) 💧"""
return self._current_volume return self._current_volume
@property @property
def max_velocity(self) -> float: def max_velocity(self) -> float:
return self._max_velocity return self._max_velocity
@property @property
def transfer_rate(self) -> float: def transfer_rate(self) -> float:
return self._transfer_rate return self._transfer_rate
@@ -103,17 +105,17 @@ class VirtualTransferPump:
"""设置最大速度 (ml/s) 🌊""" """设置最大速度 (ml/s) 🌊"""
self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内 self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内
self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s") self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s")
def get_status(self) -> str: def get_status(self) -> str:
"""获取泵状态 📋""" """获取泵状态 📋"""
return self._status return self._status
async def _simulate_operation(self, duration: float): async def _simulate_operation(self, duration: float):
"""模拟操作延时 ⏱️""" """模拟操作延时 ⏱️"""
self._status = "Busy" self._status = "Busy"
await self._ros_node.sleep(duration) await self._ros_node.sleep(duration)
self._status = "Idle" self._status = "Idle"
def _calculate_duration(self, volume: float, velocity: float = None) -> float: def _calculate_duration(self, volume: float, velocity: float = None) -> float:
""" """
计算操作持续时间 ⏰ 计算操作持续时间 ⏰
@@ -121,10 +123,10 @@ class VirtualTransferPump:
""" """
if velocity is None: if velocity is None:
velocity = self._max_velocity velocity = self._max_velocity
# 📊 计算理论时间(用于日志显示) # 📊 计算理论时间(用于日志显示)
theoretical_duration = abs(volume) / velocity theoretical_duration = abs(volume) / velocity
# 🚀 如果启用快速模式,使用固定的快速时间 # 🚀 如果启用快速模式,使用固定的快速时间
if self._fast_mode: if self._fast_mode:
# 根据操作类型选择快速时间 # 根据操作类型选择快速时间
@@ -132,13 +134,13 @@ class VirtualTransferPump:
actual_duration = self._fast_move_time actual_duration = self._fast_move_time
else: # 很小的操作 else: # 很小的操作
actual_duration = 0.5 actual_duration = 0.5
self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s") self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s")
return actual_duration return actual_duration
else: else:
# 正常模式使用理论时间 # 正常模式使用理论时间
return theoretical_duration return theoretical_duration
def _calculate_display_duration(self, volume: float, velocity: float = None) -> float: def _calculate_display_duration(self, volume: float, velocity: float = None) -> float:
""" """
计算显示用的持续时间(用于日志) 📊 计算显示用的持续时间(用于日志) 📊
@@ -147,16 +149,16 @@ class VirtualTransferPump:
if velocity is None: if velocity is None:
velocity = self._max_velocity velocity = self._max_velocity
return abs(volume) / velocity return abs(volume) / velocity
# 新的set_position方法 - 专门用于SetPumpPosition动作 # 新的set_position方法 - 专门用于SetPumpPosition动作
async def set_position(self, position: float, max_velocity: float = None): async def set_position(self, position: float, max_velocity: float = None):
""" """
移动到绝对位置 - 专门用于SetPumpPosition动作 🎯 移动到绝对位置 - 专门用于SetPumpPosition动作 🎯
Args: Args:
position (float): 目标位置 (ml) position (float): 目标位置 (ml)
max_velocity (float): 移动速度 (ml/s) max_velocity (float): 移动速度 (ml/s)
Returns: Returns:
dict: 符合SetPumpPosition.action定义的结果 dict: 符合SetPumpPosition.action定义的结果
""" """
@@ -164,19 +166,19 @@ class VirtualTransferPump:
# 验证并转换参数 # 验证并转换参数
target_position = float(position) target_position = float(position)
velocity = float(max_velocity) if max_velocity is not None else self._max_velocity velocity = float(max_velocity) if max_velocity is not None else self._max_velocity
# 限制位置在有效范围内 # 限制位置在有效范围内
target_position = max(0.0, min(float(self.max_volume), target_position)) target_position = max(0.0, min(float(self.max_volume), target_position))
# 计算移动距离 # 计算移动距离
volume_to_move = abs(target_position - self._position) volume_to_move = abs(target_position - self._position)
# 📊 计算显示用的时间(用于日志) # 📊 计算显示用的时间(用于日志)
display_duration = self._calculate_display_duration(volume_to_move, velocity) display_duration = self._calculate_display_duration(volume_to_move, velocity)
# ⚡ 计算实际执行时间(快速模式) # ⚡ 计算实际执行时间(快速模式)
actual_duration = self._calculate_duration(volume_to_move, velocity) actual_duration = self._calculate_duration(volume_to_move, velocity)
# 🎯 确定操作类型和emoji # 🎯 确定操作类型和emoji
if target_position > self._position: if target_position > self._position:
operation_type = "吸液" operation_type = "吸液"
@@ -187,28 +189,34 @@ class VirtualTransferPump:
else: else:
operation_type = "保持" operation_type = "保持"
operation_emoji = "📍" operation_emoji = "📍"
self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}") self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}")
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)") self.logger.info(
f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)"
)
self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s") self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s")
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
if self._fast_mode: if self._fast_mode:
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
# 🚀 模拟移动过程 # 🚀 模拟移动过程
if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度 if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度
start_position = self._position start_position = self._position
steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数 steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数
step_duration = actual_duration / steps step_duration = actual_duration / steps
self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}") self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}")
for i in range(steps + 1): for i in range(steps + 1):
# 计算当前位置和进度 # 计算当前位置和进度
progress = (i / steps) * 100 if steps > 0 else 100 progress = (i / steps) * 100 if steps > 0 else 100
current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position current_pos = (
start_position + (target_position - start_position) * (i / steps)
if steps > 0
else target_position
)
# 更新状态 # 更新状态
if i < steps: if i < steps:
self._status = f"{operation_type}" self._status = f"{operation_type}"
@@ -216,10 +224,10 @@ class VirtualTransferPump:
else: else:
self._status = "Idle" self._status = "Idle"
status_emoji = "" status_emoji = ""
self._position = current_pos self._position = current_pos
self._current_volume = current_pos self._current_volume = current_pos
# 显示进度每25%或最后一步) # 显示进度每25%或最后一步)
if i == 0: if i == 0:
self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%") self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%")
@@ -227,7 +235,7 @@ class VirtualTransferPump:
self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%") self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%")
elif i == steps: elif i == steps:
self.logger.info(f"{operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL") self.logger.info(f"{operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL")
# 等待一小步时间 # 等待一小步时间
if i < steps and step_duration > 0: if i < steps and step_duration > 0:
await self._ros_node.sleep(step_duration) await self._ros_node.sleep(step_duration)
@@ -236,25 +244,27 @@ class VirtualTransferPump:
self._position = target_position self._position = target_position
self._current_volume = target_position self._current_volume = target_position
self.logger.info(f" 📍 微调完成: {target_position:.2f}mL") self.logger.info(f" 📍 微调完成: {target_position:.2f}mL")
# 确保最终位置准确 # 确保最终位置准确
self._position = target_position self._position = target_position
self._current_volume = target_position self._current_volume = target_position
self._status = "Idle" self._status = "Idle"
# 📊 最终状态日志 # 📊 最终状态日志
if volume_to_move > 0.01: if volume_to_move > 0.01:
self.logger.info(f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") self.logger.info(
f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL"
)
# 返回符合action定义的结果 # 返回符合action定义的结果
return { return {
"success": True, "success": True,
"message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})", "message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})",
"final_position": self._position, "final_position": self._position,
"final_volume": self._current_volume, "final_volume": self._current_volume,
"operation_type": operation_type "operation_type": operation_type,
} }
except Exception as e: except Exception as e:
error_msg = f"❌ 设置位置失败: {str(e)}" error_msg = f"❌ 设置位置失败: {str(e)}"
self.logger.error(error_msg) self.logger.error(error_msg)
@@ -262,134 +272,136 @@ class VirtualTransferPump:
"success": False, "success": False,
"message": error_msg, "message": error_msg,
"final_position": self._position, "final_position": self._position,
"final_volume": self._current_volume "final_volume": self._current_volume,
} }
# 其他泵操作方法 # 其他泵操作方法
async def pull_plunger(self, volume: float, velocity: float = None): async def pull_plunger(self, volume: float, velocity: float = None):
""" """
拉取柱塞(吸液) 📥 拉取柱塞(吸液) 📥
Args: Args:
volume (float): 要拉取的体积 (ml) volume (float): 要拉取的体积 (ml)
velocity (float): 拉取速度 (ml/s) velocity (float): 拉取速度 (ml/s)
""" """
new_position = min(self.max_volume, self._position + volume) new_position = min(self.max_volume, self._position + volume)
actual_volume = new_position - self._position actual_volume = new_position - self._position
if actual_volume <= 0: if actual_volume <= 0:
self.logger.warning("⚠️ 无法吸液 - 已达到最大容量") self.logger.warning("⚠️ 无法吸液 - 已达到最大容量")
return return
display_duration = self._calculate_display_duration(actual_volume, velocity) display_duration = self._calculate_display_duration(actual_volume, velocity)
actual_duration = self._calculate_duration(actual_volume, velocity) actual_duration = self._calculate_duration(actual_volume, velocity)
self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL") self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL")
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL") self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
if self._fast_mode: if self._fast_mode:
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
await self._simulate_operation(actual_duration) await self._simulate_operation(actual_duration)
self._position = new_position self._position = new_position
self._current_volume = new_position self._current_volume = new_position
self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
async def push_plunger(self, volume: float, velocity: float = None): async def push_plunger(self, volume: float, velocity: float = None):
""" """
推出柱塞(排液) 📤 推出柱塞(排液) 📤
Args: Args:
volume (float): 要推出的体积 (ml) volume (float): 要推出的体积 (ml)
velocity (float): 推出速度 (ml/s) velocity (float): 推出速度 (ml/s)
""" """
new_position = max(0, self._position - volume) new_position = max(0, self._position - volume)
actual_volume = self._position - new_position actual_volume = self._position - new_position
if actual_volume <= 0: if actual_volume <= 0:
self.logger.warning("⚠️ 无法排液 - 已达到最小容量") self.logger.warning("⚠️ 无法排液 - 已达到最小容量")
return return
display_duration = self._calculate_display_duration(actual_volume, velocity) display_duration = self._calculate_display_duration(actual_volume, velocity)
actual_duration = self._calculate_duration(actual_volume, velocity) actual_duration = self._calculate_duration(actual_volume, velocity)
self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL") self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL")
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL") self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
if self._fast_mode: if self._fast_mode:
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
await self._simulate_operation(actual_duration) await self._simulate_operation(actual_duration)
self._position = new_position self._position = new_position
self._current_volume = new_position self._current_volume = new_position
self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
# 便捷操作方法 # 便捷操作方法
async def aspirate(self, volume: float, velocity: float = None): async def aspirate(self, volume: float, velocity: float = None):
"""吸液操作 📥""" """吸液操作 📥"""
await self.pull_plunger(volume, velocity) await self.pull_plunger(volume, velocity)
async def dispense(self, volume: float, velocity: float = None): async def dispense(self, volume: float, velocity: float = None):
"""排液操作 📤""" """排液操作 📤"""
await self.push_plunger(volume, velocity) await self.push_plunger(volume, velocity)
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None): async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
"""转移操作(先吸后排) 🔄""" """转移操作(先吸后排) 🔄"""
self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL") self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL")
# 吸液 # 吸液
await self.aspirate(volume, aspirate_velocity) await self.aspirate(volume, aspirate_velocity)
# 短暂停顿 # 短暂停顿
self.logger.debug("⏸️ 短暂停顿...") self.logger.debug("⏸️ 短暂停顿...")
await self._ros_node.sleep(0.1) await self._ros_node.sleep(0.1)
# 排液 # 排液
await self.dispense(volume, dispense_velocity) await self.dispense(volume, dispense_velocity)
async def empty_syringe(self, velocity: float = None): async def empty_syringe(self, velocity: float = None):
"""清空注射器""" """清空注射器"""
await self.set_position(0, velocity) await self.set_position(0, velocity)
async def fill_syringe(self, velocity: float = None): async def fill_syringe(self, velocity: float = None):
"""充满注射器""" """充满注射器"""
await self.set_position(self.max_volume, velocity) await self.set_position(self.max_volume, velocity)
async def stop_operation(self): async def stop_operation(self):
"""停止当前操作""" """停止当前操作"""
self._status = "Idle" self._status = "Idle"
self.logger.info("Operation stopped") self.logger.info("Operation stopped")
# 状态查询方法 # 状态查询方法
def get_position(self) -> float: def get_position(self) -> float:
"""获取当前位置""" """获取当前位置"""
return self._position return self._position
def get_current_volume(self) -> float: def get_current_volume(self) -> float:
"""获取当前体积""" """获取当前体积"""
return self._current_volume return self._current_volume
def get_remaining_capacity(self) -> float: def get_remaining_capacity(self) -> float:
"""获取剩余容量""" """获取剩余容量"""
return self.max_volume - self._current_volume return self.max_volume - self._current_volume
def is_empty(self) -> bool: def is_empty(self) -> bool:
"""检查是否为空""" """检查是否为空"""
return self._current_volume <= 0.01 # 允许小量误差 return self._current_volume <= 0.01 # 允许小量误差
def is_full(self) -> bool: def is_full(self) -> bool:
"""检查是否已满""" """检查是否已满"""
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差 return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
def __str__(self): def __str__(self):
return f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})" return (
f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
)
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
@@ -398,20 +410,20 @@ class VirtualTransferPump:
async def demo(): async def demo():
"""虚拟泵使用示例""" """虚拟泵使用示例"""
pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0}) pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0})
await pump.initialize() await pump.initialize()
print(f"Initial state: {pump}") print(f"Initial state: {pump}")
# 测试set_position方法 # 测试set_position方法
result = await pump.set_position(10.0, max_velocity=2.0) result = await pump.set_position(10.0, max_velocity=2.0)
print(f"Set position result: {result}") print(f"Set position result: {result}")
print(f"After setting position to 10ml: {pump}") print(f"After setting position to 10ml: {pump}")
# 吸液测试 # 吸液测试
await pump.aspirate(5.0, velocity=2.0) await pump.aspirate(5.0, velocity=2.0)
print(f"After aspirating 5ml: {pump}") print(f"After aspirating 5ml: {pump}")
# 清空测试 # 清空测试
result = await pump.set_position(0.0) result = await pump.set_position(0.0)
print(f"Empty result: {result}") print(f"Empty result: {result}")

View File

@@ -1,58 +1,72 @@
""" """
Virtual Workbench Device - 模拟工作台设备 Virtual Workbench Device - 模拟工作台设备
包含 包含:
- 1个机械臂 (每次操作3s, 独占锁) - 1个机械臂 (每次操作3s, 独占锁)
- 3个加热台 (每次加热10s, 可并行) - 3个加热台 (每次加热10s, 可并行)
工作流程 工作流程:
1. A1-A5 物料同时启动竞争机械臂 1. A1-A5 物料同时启动, 竞争机械臂
2. 机械臂将物料移动到空闲加热台 2. 机械臂将物料移动到空闲加热台
3. 加热完成后机械臂将物料移动到C1-C5 3. 加热完成后, 机械臂将物料移动到C1-C5
注意调用来自线程池使用 threading.Lock 进行同步 注意: 调用来自线程池, 使用 threading.Lock 进行同步
""" """
import logging import logging
import time import time
from typing import Dict, Any, Optional from typing import Dict, Any, Optional, List
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from threading import Lock, RLock from threading import Lock, RLock
from typing_extensions import TypedDict from typing_extensions import TypedDict
from unilabos.registry.decorators import (
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action
)
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
from unilabos.utils.decorator import not_action from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample
# ============ TypedDict 返回类型定义 ============ # ============ TypedDict 返回类型定义 ============
class MoveToHeatingStationResult(TypedDict): class MoveToHeatingStationResult(TypedDict):
"""move_to_heating_station 返回类型""" """move_to_heating_station 返回类型"""
success: bool success: bool
station_id: int station_id: int
material_id: str material_id: str
material_number: int material_number: int
message: str message: str
unilabos_samples: List[LabSample]
class StartHeatingResult(TypedDict): class StartHeatingResult(TypedDict):
"""start_heating 返回类型""" """start_heating 返回类型"""
success: bool success: bool
station_id: int station_id: int
material_id: str material_id: str
material_number: int material_number: int
message: str message: str
unilabos_samples: List[LabSample]
class MoveToOutputResult(TypedDict): class MoveToOutputResult(TypedDict):
"""move_to_output 返回类型""" """move_to_output 返回类型"""
success: bool success: bool
station_id: int station_id: int
material_id: str material_id: str
output_position: str
message: str
unilabos_samples: List[LabSample]
class PrepareMaterialsResult(TypedDict): class PrepareMaterialsResult(TypedDict):
"""prepare_materials 返回类型 - 批量准备物料""" """prepare_materials 返回类型 - 批量准备物料"""
success: bool success: bool
count: int count: int
material_1: int # 物料编号1 material_1: int # 物料编号1
@@ -61,20 +75,24 @@ class PrepareMaterialsResult(TypedDict):
material_4: int # 物料编号4 material_4: int # 物料编号4
material_5: int # 物料编号5 material_5: int # 物料编号5
message: str message: str
unilabos_samples: List[LabSample]
# ============ 状态枚举 ============ # ============ 状态枚举 ============
class HeatingStationState(Enum): class HeatingStationState(Enum):
"""加热台状态枚举""" """加热台状态枚举"""
IDLE = "idle" # 空闲 IDLE = "idle" # 空闲
OCCUPIED = "occupied" # 已放置物料等待加热 OCCUPIED = "occupied" # 已放置物料, 等待加热
HEATING = "heating" # 加热中 HEATING = "heating" # 加热中
COMPLETED = "completed" # 加热完成等待取走 COMPLETED = "completed" # 加热完成, 等待取走
class ArmState(Enum): class ArmState(Enum):
"""机械臂状态枚举""" """机械臂状态枚举"""
IDLE = "idle" # 空闲 IDLE = "idle" # 空闲
BUSY = "busy" # 工作中 BUSY = "busy" # 工作中
@@ -82,6 +100,7 @@ class ArmState(Enum):
@dataclass @dataclass
class HeatingStation: class HeatingStation:
"""加热台数据结构""" """加热台数据结构"""
station_id: int station_id: int
state: HeatingStationState = HeatingStationState.IDLE state: HeatingStationState = HeatingStationState.IDLE
current_material: Optional[str] = None # 当前物料 (如 "A1", "A2") current_material: Optional[str] = None # 当前物料 (如 "A1", "A2")
@@ -90,26 +109,31 @@ class HeatingStation:
heating_progress: float = 0.0 heating_progress: float = 0.0
@device(
id="virtual_workbench",
category=["virtual_device"],
description="Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent material processing",
)
class VirtualWorkbench: class VirtualWorkbench:
""" """
Virtual Workbench Device - 虚拟工作台设备 Virtual Workbench Device - 虚拟工作台设备
模拟一个包含1个机械臂和3个加热台的工作站 模拟一个包含1个机械臂和3个加热台的工作站
- 机械臂操作耗时3秒同一时间只能执行一个操作 - 机械臂操作耗时3秒, 同一时间只能执行一个操作
- 加热台加热耗时10秒3个加热台可并行工作 - 加热台加热耗时10秒, 3个加热台可并行工作
工作流: 工作流:
1. 物料A1-A5并发启动线程池竞争机械臂使用权 1. 物料A1-A5并发启动(线程池), 竞争机械臂使用权
2. 获取机械臂后查找空闲加热台 2. 获取机械臂后, 查找空闲加热台
3. 机械臂将物料放入加热台开始加热 3. 机械臂将物料放入加热台, 开始加热
4. 加热完成后机械臂将物料移动到目标位置Cn 4. 加热完成后, 机械臂将物料移动到目标位置Cn
""" """
_ros_node: BaseROS2DeviceNode _ros_node: BaseROS2DeviceNode
# 配置常量 # 配置常量
ARM_OPERATION_TIME: float = 3.0 # 机械臂操作时间(秒) ARM_OPERATION_TIME: float = 2 # 机械臂操作时间(秒)
HEATING_TIME: float = 10.0 # 加热时间(秒) HEATING_TIME: float = 60.0 # 加热时间(秒)
NUM_HEATING_STATIONS: int = 3 # 加热台数量 NUM_HEATING_STATIONS: int = 3 # 加热台数量
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
@@ -126,24 +150,23 @@ class VirtualWorkbench:
self.data: Dict[str, Any] = {} self.data: Dict[str, Any] = {}
# 从config中获取可配置参数 # 从config中获取可配置参数
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", 3.0)) self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", self.ARM_OPERATION_TIME))
self.HEATING_TIME = float(self.config.get("heating_time", 10.0)) self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME))
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", 3)) self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS))
# 机械臂状态和锁 (使用threading.Lock) # 机械臂状态和锁
self._arm_lock = Lock() self._arm_lock = Lock()
self._arm_state = ArmState.IDLE self._arm_state = ArmState.IDLE
self._arm_current_task: Optional[str] = None self._arm_current_task: Optional[str] = None
# 加热台状态 (station_id -> HeatingStation) - 立即初始化不依赖initialize() # 加热台状态
self._heating_stations: Dict[int, HeatingStation] = { self._heating_stations: Dict[int, HeatingStation] = {
i: HeatingStation(station_id=i) i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1)
for i in range(1, self.NUM_HEATING_STATIONS + 1)
} }
self._stations_lock = RLock() # 可重入锁,保护加热台状态 self._stations_lock = RLock()
# 任务追踪 # 任务追踪
self._active_tasks: Dict[str, Dict[str, Any]] = {} # material_id -> task_info self._active_tasks: Dict[str, Dict[str, Any]] = {}
self._tasks_lock = Lock() self._tasks_lock = Lock()
# 处理其他kwargs参数 # 处理其他kwargs参数
@@ -169,7 +192,6 @@ class VirtualWorkbench:
"""初始化虚拟工作台""" """初始化虚拟工作台"""
self.logger.info(f"初始化虚拟工作台 {self.device_id}") self.logger.info(f"初始化虚拟工作台 {self.device_id}")
# 重置加热台状态 (已在__init__中创建这里重置为初始状态)
with self._stations_lock: with self._stations_lock:
for station in self._heating_stations.values(): for station in self._heating_stations.values():
station.state = HeatingStationState.IDLE station.state = HeatingStationState.IDLE
@@ -177,15 +199,16 @@ class VirtualWorkbench:
station.material_number = None station.material_number = None
station.heating_progress = 0.0 station.heating_progress = 0.0
# 初始化状态 self.data.update(
self.data.update({ {
"status": "Ready", "status": "Ready",
"arm_state": ArmState.IDLE.value, "arm_state": ArmState.IDLE.value,
"arm_current_task": None, "arm_current_task": None,
"heating_stations": self._get_stations_status(), "heating_stations": self._get_stations_status(),
"active_tasks_count": 0, "active_tasks_count": 0,
"message": "工作台就绪", "message": "工作台就绪",
}) }
)
self.logger.info(f"工作台初始化完成: {self.NUM_HEATING_STATIONS}个加热台就绪") self.logger.info(f"工作台初始化完成: {self.NUM_HEATING_STATIONS}个加热台就绪")
return True return True
@@ -204,12 +227,14 @@ class VirtualWorkbench:
with self._tasks_lock: with self._tasks_lock:
self._active_tasks.clear() self._active_tasks.clear()
self.data.update({ self.data.update(
"status": "Offline", {
"arm_state": ArmState.IDLE.value, "status": "Offline",
"heating_stations": {}, "arm_state": ArmState.IDLE.value,
"message": "工作台已关闭", "heating_stations": {},
}) "message": "工作台已关闭",
}
)
return True return True
def _get_stations_status(self) -> Dict[int, Dict[str, Any]]: def _get_stations_status(self) -> Dict[int, Dict[str, Any]]:
@@ -227,21 +252,19 @@ class VirtualWorkbench:
def _update_data_status(self, message: Optional[str] = None): def _update_data_status(self, message: Optional[str] = None):
"""更新状态数据""" """更新状态数据"""
self.data.update({ self.data.update(
"arm_state": self._arm_state.value, {
"arm_current_task": self._arm_current_task, "arm_state": self._arm_state.value,
"heating_stations": self._get_stations_status(), "arm_current_task": self._arm_current_task,
"active_tasks_count": len(self._active_tasks), "heating_stations": self._get_stations_status(),
}) "active_tasks_count": len(self._active_tasks),
}
)
if message: if message:
self.data["message"] = message self.data["message"] = message
def _find_available_heating_station(self) -> Optional[int]: def _find_available_heating_station(self) -> Optional[int]:
"""查找空闲的加热台 """查找空闲的加热台"""
Returns:
空闲加热台ID如果没有则返回None
"""
with self._stations_lock: with self._stations_lock:
for station_id, station in self._heating_stations.items(): for station_id, station in self._heating_stations.items():
if station.state == HeatingStationState.IDLE: if station.state == HeatingStationState.IDLE:
@@ -249,23 +272,12 @@ class VirtualWorkbench:
return None return None
def _acquire_arm(self, task_description: str) -> bool: def _acquire_arm(self, task_description: str) -> bool:
"""获取机械臂使用权阻塞直到获取 """获取机械臂使用权(阻塞直到获取)"""
Args:
task_description: 任务描述,用于日志
Returns:
是否成功获取
"""
self.logger.info(f"[{task_description}] 等待获取机械臂...") self.logger.info(f"[{task_description}] 等待获取机械臂...")
# 阻塞等待获取锁
self._arm_lock.acquire() self._arm_lock.acquire()
self._arm_state = ArmState.BUSY self._arm_state = ArmState.BUSY
self._arm_current_task = task_description self._arm_current_task = task_description
self._update_data_status(f"机械臂执行: {task_description}") self._update_data_status(f"机械臂执行: {task_description}")
self.logger.info(f"[{task_description}] 成功获取机械臂使用权") self.logger.info(f"[{task_description}] 成功获取机械臂使用权")
return True return True
@@ -278,28 +290,37 @@ class VirtualWorkbench:
self._update_data_status(f"机械臂已释放 (完成: {task})") self._update_data_status(f"机械臂已释放 (完成: {task})")
self.logger.info(f"机械臂已释放 (完成: {task})") self.logger.info(f"机械臂已释放 (完成: {task})")
@action(
auto_prefix=True,
description="批量准备物料 - 虚拟起始节点, 生成A1-A5物料, 输出5个handle供后续节点使用",
handles=[
ActionOutputHandle(key="channel_1", data_type="workbench_material",
label="实验1", data_key="material_1", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="channel_2", data_type="workbench_material",
label="实验2", data_key="material_2", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="channel_3", data_type="workbench_material",
label="实验3", data_key="material_3", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="channel_4", data_type="workbench_material",
label="实验4", data_key="material_4", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="channel_5", data_type="workbench_material",
label="实验5", data_key="material_5", data_source=DataSource.EXECUTOR),
],
)
def prepare_materials( def prepare_materials(
self, self,
sample_uuids: SampleUUIDsType,
count: int = 5, count: int = 5,
) -> PrepareMaterialsResult: ) -> PrepareMaterialsResult:
""" """
批量准备物料 - 虚拟起始节点 批量准备物料 - 虚拟起始节点
作为工作流的起始节点生成指定数量的物料编号供后续节点使用。 作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。
输出5个handle (material_1 ~ material_5)分别对应实验1~5。 输出5个handle (material_1 ~ material_5), 分别对应实验1~5。
Args:
count: 待生成的物料数量默认5 (生成 A1-A5)
Returns:
PrepareMaterialsResult: 包含 material_1 ~ material_5 用于传递给 move_to_heating_station
""" """
# 生成物料列表 A1 - A{count}
materials = [i for i in range(1, count + 1)] materials = [i for i in range(1, count + 1)]
self.logger.info( self.logger.info(
f"[准备物料] 生成 {count} 个物料: " f"[准备物料] 生成 {count} 个物料: A1-A{count} -> material_1~material_{count}"
f"A1-A{count} -> material_1~material_{count}"
) )
return { return {
@@ -311,29 +332,42 @@ class VirtualWorkbench:
"material_4": materials[3] if len(materials) > 3 else 0, "material_4": materials[3] if len(materials) > 3 else 0,
"material_5": materials[4] if len(materials) > 4 else 0, "material_5": materials[4] if len(materials) > 4 else 0,
"message": f"已准备 {count} 个物料: A1-A{count}", "message": f"已准备 {count} 个物料: A1-A{count}",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra={"material_uuid": content} if isinstance(content, str) else (content.serialize() if content else {}),
)
for sample_uuid, content in sample_uuids.items()
],
} }
@action(
auto_prefix=True,
description="将物料从An位置移动到空闲加热台, 返回分配的加热台ID",
handles=[
ActionInputHandle(key="material_input", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
ActionOutputHandle(key="heating_station_output", data_type="workbench_station",
label="加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="material_number_output", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
],
)
def move_to_heating_station( def move_to_heating_station(
self, self,
sample_uuids: SampleUUIDsType,
material_number: int, material_number: int,
) -> MoveToHeatingStationResult: ) -> MoveToHeatingStationResult:
""" """
将物料从An位置移动到加热台 将物料从An位置移动到加热台
多线程并发调用时会竞争机械臂使用权并自动查找空闲加热台 多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台
Args:
material_number: 物料编号 (1-5)
Returns:
MoveToHeatingStationResult: 包含 station_id, material_number 等用于传递给下一个节点
""" """
# 根据物料编号生成物料ID
material_id = f"A{material_number}" material_id = f"A{material_number}"
task_desc = f"移动{material_id}到加热台" task_desc = f"移动{material_id}到加热台"
self.logger.info(f"[任务] {task_desc} - 开始执行") self.logger.info(f"[任务] {task_desc} - 开始执行")
# 记录任务
with self._tasks_lock: with self._tasks_lock:
self._active_tasks[material_id] = { self._active_tasks[material_id] = {
"status": "waiting_for_arm", "status": "waiting_for_arm",
@@ -341,33 +375,27 @@ class VirtualWorkbench:
} }
try: try:
# 步骤1: 等待获取机械臂使用权(竞争)
with self._tasks_lock: with self._tasks_lock:
self._active_tasks[material_id]["status"] = "waiting_for_arm" self._active_tasks[material_id]["status"] = "waiting_for_arm"
self._acquire_arm(task_desc) self._acquire_arm(task_desc)
# 步骤2: 查找空闲加热台
with self._tasks_lock: with self._tasks_lock:
self._active_tasks[material_id]["status"] = "finding_station" self._active_tasks[material_id]["status"] = "finding_station"
station_id = None station_id = None
# 循环等待直到找到空闲加热台
while station_id is None: while station_id is None:
station_id = self._find_available_heating_station() station_id = self._find_available_heating_station()
if station_id is None: if station_id is None:
self.logger.info(f"[{material_id}] 没有空闲加热台等待中...") self.logger.info(f"[{material_id}] 没有空闲加热台, 等待中...")
# 释放机械臂,等待后重试
self._release_arm() self._release_arm()
time.sleep(0.5) time.sleep(0.5)
self._acquire_arm(task_desc) self._acquire_arm(task_desc)
# 步骤3: 占用加热台 - 立即标记为OCCUPIED防止其他任务选择同一加热台
with self._stations_lock: with self._stations_lock:
self._heating_stations[station_id].state = HeatingStationState.OCCUPIED self._heating_stations[station_id].state = HeatingStationState.OCCUPIED
self._heating_stations[station_id].current_material = material_id self._heating_stations[station_id].current_material = material_id
self._heating_stations[station_id].material_number = material_number self._heating_stations[station_id].material_number = material_number
# 步骤4: 模拟机械臂移动操作 (3秒)
with self._tasks_lock: with self._tasks_lock:
self._active_tasks[material_id]["status"] = "arm_moving" self._active_tasks[material_id]["status"] = "arm_moving"
self._active_tasks[material_id]["assigned_station"] = station_id self._active_tasks[material_id]["assigned_station"] = station_id
@@ -375,11 +403,11 @@ class VirtualWorkbench:
time.sleep(self.ARM_OPERATION_TIME) time.sleep(self.ARM_OPERATION_TIME)
# 步骤5: 放入加热台完成
self._update_data_status(f"{material_id}已放入加热台{station_id}") self._update_data_status(f"{material_id}已放入加热台{station_id}")
self.logger.info(f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)") self.logger.info(
f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)"
)
# 释放机械臂
self._release_arm() self._release_arm()
with self._tasks_lock: with self._tasks_lock:
@@ -391,6 +419,17 @@ class VirtualWorkbench:
"material_id": material_id, "material_id": material_id,
"material_number": material_number, "material_number": material_number,
"message": f"{material_id}已成功移动到加热台{station_id}", "message": f"{material_id}已成功移动到加热台{station_id}",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
except Exception as e: except Exception as e:
@@ -403,22 +442,42 @@ class VirtualWorkbench:
"material_id": material_id, "material_id": material_id,
"material_number": material_number, "material_number": material_number,
"message": f"移动失败: {str(e)}", "message": f"移动失败: {str(e)}",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
@action(
auto_prefix=True,
always_free=True,
description="启动指定加热台的加热程序",
handles=[
ActionInputHandle(key="station_id_input", data_type="workbench_station",
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
ActionInputHandle(key="material_number_input", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
ActionOutputHandle(key="heating_done_station", data_type="workbench_station",
label="加热完成-加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="heating_done_material", data_type="workbench_material",
label="加热完成-物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
],
)
def start_heating( def start_heating(
self, self,
sample_uuids: SampleUUIDsType,
station_id: int, station_id: int,
material_number: int, material_number: int,
) -> StartHeatingResult: ) -> StartHeatingResult:
""" """
启动指定加热台的加热程序 启动指定加热台的加热程序
Args:
station_id: 加热台ID (1-3),从 move_to_heating_station 的 handle 传入
material_number: 物料编号,从 move_to_heating_station 的 handle 传入
Returns:
StartHeatingResult: 包含 station_id, material_number 等用于传递给下一个节点
""" """
self.logger.info(f"[加热台{station_id}] 开始加热") self.logger.info(f"[加热台{station_id}] 开始加热")
@@ -429,6 +488,17 @@ class VirtualWorkbench:
"material_id": "", "material_id": "",
"material_number": material_number, "material_number": material_number,
"message": f"无效的加热台ID: {station_id}", "message": f"无效的加热台ID: {station_id}",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
with self._stations_lock: with self._stations_lock:
@@ -441,6 +511,17 @@ class VirtualWorkbench:
"material_id": "", "material_id": "",
"material_number": material_number, "material_number": material_number,
"message": f"加热台{station_id}上没有物料", "message": f"加热台{station_id}上没有物料",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
if station.state == HeatingStationState.HEATING: if station.state == HeatingStationState.HEATING:
@@ -450,11 +531,21 @@ class VirtualWorkbench:
"material_id": station.current_material, "material_id": station.current_material,
"material_number": material_number, "material_number": material_number,
"message": f"加热台{station_id}已经在加热中", "message": f"加热台{station_id}已经在加热中",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
material_id = station.current_material material_id = station.current_material
# 开始加热
station.state = HeatingStationState.HEATING station.state = HeatingStationState.HEATING
station.heating_start_time = time.time() station.heating_start_time = time.time()
station.heating_progress = 0.0 station.heating_progress = 0.0
@@ -465,10 +556,19 @@ class VirtualWorkbench:
self._update_data_status(f"加热台{station_id}开始加热{material_id}") self._update_data_status(f"加热台{station_id}开始加热{material_id}")
# 模拟加热过程 (10秒) with self._stations_lock:
heating_list = [
f"加热台{sid}:{s.current_material}"
for sid, s in self._heating_stations.items()
if s.state == HeatingStationState.HEATING and s.current_material
]
self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}")
start_time = time.time() start_time = time.time()
last_countdown_log = start_time
while True: while True:
elapsed = time.time() - start_time elapsed = time.time() - start_time
remaining = max(0.0, self.HEATING_TIME - elapsed)
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100) progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
with self._stations_lock: with self._stations_lock:
@@ -476,12 +576,15 @@ class VirtualWorkbench:
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%") self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
if time.time() - last_countdown_log >= 5.0:
self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s")
last_countdown_log = time.time()
if elapsed >= self.HEATING_TIME: if elapsed >= self.HEATING_TIME:
break break
time.sleep(1.0) time.sleep(1.0)
# 加热完成
with self._stations_lock: with self._stations_lock:
self._heating_stations[station_id].state = HeatingStationState.COMPLETED self._heating_stations[station_id].state = HeatingStationState.COMPLETED
self._heating_stations[station_id].heating_progress = 100.0 self._heating_stations[station_id].heating_progress = 100.0
@@ -499,24 +602,39 @@ class VirtualWorkbench:
"material_id": material_id, "material_id": material_id,
"material_number": material_number, "material_number": material_number,
"message": f"加热台{station_id}加热完成", "message": f"加热台{station_id}加热完成",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
@action(
auto_prefix=True,
description="将物料从加热台移动到输出位置Cn",
handles=[
ActionInputHandle(key="output_station_input", data_type="workbench_station",
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
ActionInputHandle(key="output_material_input", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
],
)
def move_to_output( def move_to_output(
self, self,
sample_uuids: SampleUUIDsType,
station_id: int, station_id: int,
material_number: int, material_number: int,
) -> MoveToOutputResult: ) -> MoveToOutputResult:
""" """
将物料从加热台移动到输出位置Cn 将物料从加热台移动到输出位置Cn
Args:
station_id: 加热台ID (1-3),从 start_heating 的 handle 传入
material_number: 物料编号,从 start_heating 的 handle 传入,用于确定输出位置 Cn
Returns:
MoveToOutputResult: 包含执行结果
""" """
output_number = material_number # 物料编号决定输出位置 output_number = material_number
if station_id not in self._heating_stations: if station_id not in self._heating_stations:
return { return {
@@ -525,6 +643,17 @@ class VirtualWorkbench:
"material_id": "", "material_id": "",
"output_position": f"C{output_number}", "output_position": f"C{output_number}",
"message": f"无效的加热台ID: {station_id}", "message": f"无效的加热台ID: {station_id}",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
with self._stations_lock: with self._stations_lock:
@@ -538,6 +667,17 @@ class VirtualWorkbench:
"material_id": "", "material_id": "",
"output_position": f"C{output_number}", "output_position": f"C{output_number}",
"message": f"加热台{station_id}上没有物料", "message": f"加热台{station_id}上没有物料",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
if station.state != HeatingStationState.COMPLETED: if station.state != HeatingStationState.COMPLETED:
@@ -547,6 +687,17 @@ class VirtualWorkbench:
"material_id": material_id, "material_id": material_id,
"output_position": f"C{output_number}", "output_position": f"C{output_number}",
"message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})", "message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
output_position = f"C{output_number}" output_position = f"C{output_number}"
@@ -558,18 +709,17 @@ class VirtualWorkbench:
if material_id in self._active_tasks: if material_id in self._active_tasks:
self._active_tasks[material_id]["status"] = "waiting_for_arm_output" self._active_tasks[material_id]["status"] = "waiting_for_arm_output"
# 获取机械臂
self._acquire_arm(task_desc) self._acquire_arm(task_desc)
with self._tasks_lock: with self._tasks_lock:
if material_id in self._active_tasks: if material_id in self._active_tasks:
self._active_tasks[material_id]["status"] = "arm_moving_to_output" self._active_tasks[material_id]["status"] = "arm_moving_to_output"
# 模拟机械臂操作 (3秒) self.logger.info(
self.logger.info(f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}...") f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}..."
)
time.sleep(self.ARM_OPERATION_TIME) time.sleep(self.ARM_OPERATION_TIME)
# 清空加热台
with self._stations_lock: with self._stations_lock:
self._heating_stations[station_id].state = HeatingStationState.IDLE self._heating_stations[station_id].state = HeatingStationState.IDLE
self._heating_stations[station_id].current_material = None self._heating_stations[station_id].current_material = None
@@ -577,17 +727,17 @@ class VirtualWorkbench:
self._heating_stations[station_id].heating_progress = 0.0 self._heating_stations[station_id].heating_progress = 0.0
self._heating_stations[station_id].heating_start_time = None self._heating_stations[station_id].heating_start_time = None
# 释放机械臂
self._release_arm() self._release_arm()
# 任务完成
with self._tasks_lock: with self._tasks_lock:
if material_id in self._active_tasks: if material_id in self._active_tasks:
self._active_tasks[material_id]["status"] = "completed" self._active_tasks[material_id]["status"] = "completed"
self._active_tasks[material_id]["end_time"] = time.time() self._active_tasks[material_id]["end_time"] = time.time()
self._update_data_status(f"{material_id}已移动到{output_position}") self._update_data_status(f"{material_id}已移动到{output_position}")
self.logger.info(f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)") self.logger.info(
f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)"
)
return { return {
"success": True, "success": True,
@@ -595,6 +745,18 @@ class VirtualWorkbench:
"material_id": material_id, "material_id": material_id,
"output_position": output_position, "output_position": output_position,
"message": f"{material_id}已成功移动到{output_position}", "message": f"{material_id}已成功移动到{output_position}",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str)
else (content.serialize() if content is not None else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
except Exception as e: except Exception as e:
@@ -607,81 +769,106 @@ class VirtualWorkbench:
"material_id": "", "material_id": "",
"output_position": output_position, "output_position": output_position,
"message": f"移动失败: {str(e)}", "message": f"移动失败: {str(e)}",
"unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
# ============ 状态属性 ============ # ============ 状态属性 ============
@property @property
@topic_config()
def status(self) -> str: def status(self) -> str:
return self.data.get("status", "Unknown") return self.data.get("status", "Unknown")
@property @property
@topic_config()
def arm_state(self) -> str: def arm_state(self) -> str:
return self._arm_state.value return self._arm_state.value
@property @property
@topic_config()
def arm_current_task(self) -> str: def arm_current_task(self) -> str:
return self._arm_current_task or "" return self._arm_current_task or ""
@property @property
@topic_config()
def heating_station_1_state(self) -> str: def heating_station_1_state(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(1) station = self._heating_stations.get(1)
return station.state.value if station else "unknown" return station.state.value if station else "unknown"
@property @property
@topic_config()
def heating_station_1_material(self) -> str: def heating_station_1_material(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(1) station = self._heating_stations.get(1)
return station.current_material or "" if station else "" return station.current_material or "" if station else ""
@property @property
@topic_config()
def heating_station_1_progress(self) -> float: def heating_station_1_progress(self) -> float:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(1) station = self._heating_stations.get(1)
return station.heating_progress if station else 0.0 return station.heating_progress if station else 0.0
@property @property
@topic_config()
def heating_station_2_state(self) -> str: def heating_station_2_state(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(2) station = self._heating_stations.get(2)
return station.state.value if station else "unknown" return station.state.value if station else "unknown"
@property @property
@topic_config()
def heating_station_2_material(self) -> str: def heating_station_2_material(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(2) station = self._heating_stations.get(2)
return station.current_material or "" if station else "" return station.current_material or "" if station else ""
@property @property
@topic_config()
def heating_station_2_progress(self) -> float: def heating_station_2_progress(self) -> float:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(2) station = self._heating_stations.get(2)
return station.heating_progress if station else 0.0 return station.heating_progress if station else 0.0
@property @property
@topic_config()
def heating_station_3_state(self) -> str: def heating_station_3_state(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(3) station = self._heating_stations.get(3)
return station.state.value if station else "unknown" return station.state.value if station else "unknown"
@property @property
@topic_config()
def heating_station_3_material(self) -> str: def heating_station_3_material(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(3) station = self._heating_stations.get(3)
return station.current_material or "" if station else "" return station.current_material or "" if station else ""
@property @property
@topic_config()
def heating_station_3_progress(self) -> float: def heating_station_3_progress(self) -> float:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(3) station = self._heating_stations.get(3)
return station.heating_progress if station else 0.0 return station.heating_progress if station else 0.0
@property @property
@topic_config()
def active_tasks_count(self) -> int: def active_tasks_count(self) -> int:
with self._tasks_lock: with self._tasks_lock:
return len(self._active_tasks) return len(self._active_tasks)
@property @property
@topic_config()
def message(self) -> str: def message(self) -> str:
return self.data.get("message", "") return self.data.get("message", "")

View File

@@ -1,5 +1,9 @@
# 工作站抽象基类物料系统架构说明 # 工作站抽象基类物料系统架构说明
## 设计理念
基于用户需求"请你帮我系统思考一下,工作站抽象基类的物料系统基类该如何构建",我们最终确定了一个**PyLabRobot Deck为中心**的简化架构。
### 核心原则 ### 核心原则
1. **PyLabRobot为物料管理核心**使用PyLabRobot的Deck系统作为物料管理的基础利用其成熟的Resource体系 1. **PyLabRobot为物料管理核心**使用PyLabRobot的Deck系统作为物料管理的基础利用其成熟的Resource体系

View File

@@ -1,113 +0,0 @@
# Bioyond Cell 工作站 - 多订单返回示例
本文档说明了 `create_orders` 函数如何收集并返回所有订单的完成报文。
## 问题描述
之前的实现只会等待并返回第一个订单的完成报文,如果有多个订单(例如从 Excel 解析出 3 个订单),只能得到第一个订单的推送信息。
## 解决方案
修改后的 `create_orders` 函数现在会:
1. **提取所有 orderCode**:从 LIMS 接口返回的 `data` 列表中提取所有订单编号
2. **逐个等待完成**:遍历所有 orderCode调用 `wait_for_order_finish` 等待每个订单完成
3. **收集所有报文**:将每个订单的完成报文存入 `all_reports` 列表
4. **统一返回**:返回包含所有订单报文的 JSON 格式数据
## 返回格式
```json
{
"status": "all_completed",
"total_orders": 3,
"reports": [
{
"token": "",
"request_time": "2025-12-24T15:32:09.2148671+08:00",
"data": {
"orderId": "3a1e614d-a082-c44a-60be-68647a35e6f1",
"orderCode": "BSO2025122400024",
"orderName": "DP20251224001",
"status": "30",
"workflowStatus": "completed",
"usedMaterials": [...]
}
},
{
"token": "",
"request_time": "2025-12-24T15:32:09.9999039+08:00",
"data": {
"orderId": "3a1e614d-a0a2-f7a9-9360-610021c9479d",
"orderCode": "BSO2025122400025",
"orderName": "DP20251224002",
"status": "30",
"workflowStatus": "completed",
"usedMaterials": [...]
}
},
{
"token": "",
"request_time": "2025-12-24T15:34:00.4139986+08:00",
"data": {
"orderId": "3a1e614d-a0cd-81ca-9f7f-2f4e93af01cd",
"orderCode": "BSO2025122400026",
"orderName": "DP20251224003",
"status": "30",
"workflowStatus": "completed",
"usedMaterials": [...]
}
}
],
"original_response": {...}
}
```
## 使用示例
```python
# 调用 create_orders
result = workstation.create_orders("20251224.xlsx")
# 访问返回数据
print(f"总订单数: {result['total_orders']}")
print(f"状态: {result['status']}")
# 遍历所有订单的报文
for i, report in enumerate(result['reports'], 1):
order_data = report.get('data', {})
print(f"\n订单 {i}:")
print(f" orderCode: {order_data.get('orderCode')}")
print(f" orderName: {order_data.get('orderName')}")
print(f" status: {order_data.get('status')}")
print(f" 使用物料数: {len(order_data.get('usedMaterials', []))}")
```
## 控制台输出示例
```
[create_orders] 即将提交订单数量: 3
[create_orders] 接口返回: {...}
[create_orders] 等待 3 个订单完成: ['BSO2025122400024', 'BSO2025122400025', 'BSO2025122400026']
[create_orders] 正在等待第 1/3 个订单: BSO2025122400024
[create_orders] ✓ 订单 BSO2025122400024 完成
[create_orders] 正在等待第 2/3 个订单: BSO2025122400025
[create_orders] ✓ 订单 BSO2025122400025 完成
[create_orders] 正在等待第 3/3 个订单: BSO2025122400026
[create_orders] ✓ 订单 BSO2025122400026 完成
[create_orders] 所有订单已完成,共收集 3 个报文
实验记录本========================create_orders========================
返回报文数量: 3
报文 1: orderCode=BSO2025122400024, status=30
报文 2: orderCode=BSO2025122400025, status=30
报文 3: orderCode=BSO2025122400026, status=30
========================
```
## 关键改进
1.**等待所有订单**:不再只等待第一个订单,而是遍历所有 orderCode
2.**收集完整报文**:每个订单的完整推送报文都被保存在 `reports` 数组中
3.**详细日志**:清晰显示正在等待哪个订单,以及完成情况
4.**错误处理**:即使某个订单失败,也会记录其状态信息
5.**统一格式**:返回的 JSON 格式便于后续处理和分析

View File

@@ -1,204 +0,0 @@
# BioyondCellWorkstation JSON 配置迁移经验总结
**日期**: 2026-01-13
**目的**: 从 `config.py` 迁移到 JSON 配置文件
---
## 问题背景
原系统通过 `config.py` 管理配置,导致:
1. HTTP 服务重复启动(父类 `BioyondWorkstation` 和子类都启动)
2. 配置分散在代码中,不便于管理
3. 无法通过 JSON 统一配置所有参数
---
## 解决方案:嵌套配置结构
### JSON 结构设计
**正确示例** (嵌套在 `config` 中):
```json
{
"nodes": [{
"id": "bioyond_cell_workstation",
"config": {
"deck": {...},
"protocol_type": [],
"bioyond_config": {
"api_host": "http://172.16.11.219:44388",
"api_key": "8A819E5C",
"timeout": 30,
"HTTP_host": "172.16.11.206",
"HTTP_port": 8080,
"debug_mode": false,
"material_type_mappings": {...},
"warehouse_mapping": {...},
"solid_liquid_mappings": {...}
}
},
"data": {}
}]
}
```
**关键点**
-`bioyond_config` 放在 `config` 中(会传递到 `__init__`
- ❌ **不要**放在 `data` 中(`data` 是运行时状态,不会传递)
---
## Python 代码适配
### 1. 修改 `BioyondCellWorkstation.__init__` 签名
**文件**: `bioyond_cell_workstation.py`
```python
def __init__(self, bioyond_config: dict = None, deck=None, protocol_type=None, **kwargs):
"""
Args:
bioyond_config: 从 JSON 加载的配置字典
deck: Deck 配置
protocol_type: 协议类型
"""
# 验证配置
if bioyond_config is None:
raise ValueError("需要 bioyond_config 参数")
# 保存配置
self.bioyond_config = bioyond_config
# 设置 HTTP 服务去重标志
self.bioyond_config["_disable_auto_http_service"] = True
# 调用父类
super().__init__(bioyond_config=self.bioyond_config, deck=deck, **kwargs)
```
### 2. 替换全局变量引用
**修改前**(使用全局变量):
```python
from config import MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING
def create_sample(self, board_type, ...):
carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1]
location_id = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"][location_code]
```
**修改后**(从配置读取):
```python
def create_sample(self, board_type, ...):
carrier_type_id = self.bioyond_config['material_type_mappings'][board_type][1]
location_id = self.bioyond_config['warehouse_mapping'][warehouse_name]["site_uuids"][location_code]
```
### 3. 修复父类配置访问
`station.py` 中安全访问配置默认值:
```python
# 修改前(会 KeyError
self._http_service_config = {
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG["http_service_host"])
}
# 修改后(安全访问)
self._http_service_config = {
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG.get("http_service_host", ""))
}
```
---
## 常见陷阱
### ❌ 错误1将配置放在 `data` 字段
```json
"config": {"deck": {...}},
"data": {"bioyond_config": {...}} // ❌ 不会传递到 __init__
```
### ❌ 错误2扁平化配置已废弃方案
虽然扁平化也能工作,但不推荐:
```json
"config": {
"deck": {...},
"api_host": "...", // ❌ 不够清晰
"api_key": "...",
"HTTP_host": "..."
}
```
### ❌ 错误3忘记替换全局变量引用
代码中直接使用 `MATERIAL_TYPE_MAPPINGS` 等全局变量会导致 `NameError`
---
## 云端同步注意事项
使用 `--upload_registry` 时,云端配置可能覆盖本地配置:
- 首次上传时确保 JSON 完整
- 或使用新的 `ak/sk` 避免旧配置干扰
- 调试时可暂时移除 `--upload_registry` 参数
---
## 验证清单
启动成功后应看到:
```
✅ 从 JSON 配置加载 bioyond_config 成功
API Host: http://...
HTTP Service: ...
✅ BioyondCellWorkstation 初始化完成
Loaded ResourceTreeSet with ... nodes
```
运行时不应出现:
-`NameError: name 'MATERIAL_TYPE_MAPPINGS' is not defined`
-`KeyError: 'http_service_host'`
-`bioyond_config 缺少必需参数`
---
## 调试经验
1. **添加调试日志**查看参数传递链路:
- `graphio.py`: JSON 加载后的 config 内容
- `initialize_device.py`: `device_config.res_content.config` 的键
- `bioyond_cell_workstation.py`: `__init__` 接收到的参数
2. **config vs data 区别**
- `config`: 初始化参数,传递给 `__init__`
- `data`: 运行时状态,不传递给 `__init__`
3. **参数名必须匹配**
- JSON 中的键名必须与 `__init__` 参数名完全一致
4. **调试代码清理**:完成后记得删除调试日志(🔍 DEBUG 标记)
---
## 修改文件清单
| 文件 | 修改内容 |
|------|----------|
| `yibin_electrolyte_config.json` | 创建嵌套 `config.bioyond_config` 结构 |
| `bioyond_cell_workstation.py` | 修改 `__init__` 接收 `bioyond_config`,替换所有全局变量引用 |
| `station.py` | 安全访问 `HTTP_SERVICE_CONFIG` 默认值 |
---
## 参考代码位置
- JSON 配置示例: `yibin_electrolyte_config.json` L12-L353
- `__init__` 实现: `bioyond_cell_workstation.py` L39-L94
- 全局变量替换示例: `bioyond_cell_workstation.py` L2005, L1863, L1966
- HTTP 服务配置: `station.py` L629-L634
---
**总结**: 使用嵌套结构将所有配置放在 `config.bioyond_config` 中,修改 `__init__` 直接接收该参数,并替换所有全局变量引用为 `self.bioyond_config` 访问。

View File

@@ -1,312 +0,0 @@
# BioyondCell 配置迁移修改总结
**日期**: 2026-01-13
**目标**: 从 `config.py` 完全迁移到 JSON 配置,消除所有全局变量依赖
---
## 📋 修改概览
本次修改完成了 BioyondCell 模块从 Python 配置文件到 JSON 配置的完整迁移,并清理了所有对 `config.py` 全局变量的依赖。
### 核心成果
- ✅ 完全移除对 `config.py` 的导入依赖
- ✅ 使用嵌套 JSON 结构 `config.bioyond_config`
- ✅ 修复 7 处 `bioyond_cell_workstation.py` 中的全局变量引用
- ✅ 修复 3 处其他文件中的全局变量引用
- ✅ HTTP 服务去重机制完善
- ✅ 系统成功启动并正常运行
---
## 🔧 修改文件清单
### 1. JSON 配置文件
**文件**: `yibin_electrolyte_config.json`
**修改**:
- 采用嵌套结构将所有配置放在 `config.bioyond_config`
- 包含:`api_host`, `api_key`, `HTTP_host`, `HTTP_port`, `material_type_mappings`, `warehouse_mapping`, `solid_liquid_mappings`
**示例结构**:
```json
{
"nodes": [{
"id": "bioyond_cell_workstation",
"config": {
"deck": {...},
"protocol_type": [],
"bioyond_config": {
"api_host": "http://172.16.11.219:44388",
"api_key": "8A819E5C",
"HTTP_host": "172.16.11.206",
"HTTP_port": 8080,
"material_type_mappings": {...},
"warehouse_mapping": {...},
"solid_liquid_mappings": {...}
}
}
}]
}
```
---
### 2. bioyond_cell_workstation.py
**位置**: `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
#### 修改 A: `__init__` 方法签名 (L39-99)
**修改前**:
```python
def __init__(self, deck=None, protocol_type=None, **kwargs):
# 从 kwargs 收集配置字段
self.bioyond_config = {}
for field in bioyond_field_names:
if field in kwargs:
self.bioyond_config[field] = kwargs.pop(field)
```
**修改后**:
```python
def __init__(self, bioyond_config: dict = None, deck=None, protocol_type=None, **kwargs):
"""直接接收 bioyond_config 参数"""
if bioyond_config is None:
raise ValueError("需要 bioyond_config 参数")
self.bioyond_config = bioyond_config
# 设置 HTTP 服务去重标志
self.bioyond_config["_disable_auto_http_service"] = True
super().__init__(bioyond_config=self.bioyond_config, deck=deck, **kwargs)
```
#### 修改 B: 替换全局变量引用 (7 处)
| 位置 | 原代码 | 修改后 |
|------|--------|--------|
| L2005 | `MATERIAL_TYPE_MAPPINGS[board_type][1]` | `self.bioyond_config['material_type_mappings'][board_type][1]` |
| L2006 | `MATERIAL_TYPE_MAPPINGS[bottle_type][1]` | `self.bioyond_config['material_type_mappings'][bottle_type][1]` |
| L2009 | `WAREHOUSE_MAPPING` | `self.bioyond_config['warehouse_mapping']` |
| L2013 | `WAREHOUSE_MAPPING[warehouse_name]` | `self.bioyond_config['warehouse_mapping'][warehouse_name]` |
| L2017 | `WAREHOUSE_MAPPING[warehouse_name]["site_uuids"]` | `self.bioyond_config['warehouse_mapping'][warehouse_name]["site_uuids"]` |
| L1863 | `SOLID_LIQUID_MAPPINGS.get(material_name)` | `self.bioyond_config.get('solid_liquid_mappings', {}).get(material_name)` |
| L1966, L1976 | `MATERIAL_TYPE_MAPPINGS.items()` | `self.bioyond_config['material_type_mappings'].items()` |
---
### 3. station.py
**位置**: `unilabos/devices/workstation/bioyond_studio/station.py`
#### 修改 A: 删除 config 导入 (L26-28)
**修改前**:
```python
from unilabos.devices.workstation.bioyond_studio.config import (
API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, HTTP_SERVICE_CONFIG
)
```
**修改后**:
```python
# 已删除此导入
```
#### 修改 B: `_create_communication_module` 方法 (L691-702)
**修改前**:
```python
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
default_config = {
**API_CONFIG,
"workflow_mappings": WORKFLOW_MAPPINGS,
"material_type_mappings": MATERIAL_TYPE_MAPPINGS,
"warehouse_mapping": WAREHOUSE_MAPPING
}
if config:
self.bioyond_config = {**default_config, **config}
else:
self.bioyond_config = default_config
```
**修改后**:
```python
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
"""创建Bioyond通信模块"""
# 使用传入的 config 参数(来自 bioyond_config
# 不再依赖全局变量 API_CONFIG 等
if config:
self.bioyond_config = config
else:
# 如果没有传入配置,创建空配置(用于测试或兼容性)
self.bioyond_config = {}
self.hardware_interface = BioyondV1RPC(self.bioyond_config)
```
#### 修改 C: HTTP 服务配置 (L627-632)
**修改前**:
```python
self._http_service_config = {
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG.get("http_service_host", "")),
"port": bioyond_config.get("http_service_port", HTTP_SERVICE_CONFIG.get("http_service_port", 0))
}
```
**修改后**:
```python
self._http_service_config = {
"host": bioyond_config.get("http_service_host", bioyond_config.get("HTTP_host", "")),
"port": bioyond_config.get("http_service_port", bioyond_config.get("HTTP_port", 0))
}
```
---
### 4. bioyond_rpc.py
**位置**: `unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py`
#### 修改 A: 删除 config 导入 (L12)
**修改前**:
```python
from unilabos.devices.workstation.bioyond_studio.config import LOCATION_MAPPING
```
**修改后**:
```python
# 已删除此导入
```
#### 修改 B: `material_outbound` 方法 (L278-280)
**修改前**:
```python
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
"""指定库位出库物料(通过库位名称)"""
location_id = LOCATION_MAPPING.get(location_name, location_name)
```
**修改后**:
```python
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
"""指定库位出库物料(通过库位名称)"""
# location_name 参数实际上应该直接是 location_id (UUID)
location_id = location_name
```
**说明**: `LOCATION_MAPPING``config-0113.py` 中本来就是空字典 `{}`,所以直接使用 `location_name` 逻辑等价。
---
## 🎯 关键设计决策
### 1. 嵌套 vs 扁平配置
**选择**: 嵌套结构 `config.bioyond_config`
**理由**:
- ✅ 语义清晰,配置分组明确
- ✅ 参数传递直观,直接对应 `__init__` 参数
- ✅ 易于维护,不需要硬编码字段列表
- ✅ 符合 UniLab 设计模式
### 2. HTTP 服务去重
**实现**: 子类设置 `_disable_auto_http_service` 标志
```python
# bioyond_cell_workstation.py
self.bioyond_config["_disable_auto_http_service"] = True
# station.py (post_init)
if self.bioyond_config.get("_disable_auto_http_service"):
logger.info("子类已自行管理HTTP服务跳过自动启动")
return
```
### 3. 全局变量替换策略
**原则**: 所有配置从 `self.bioyond_config` 获取
**模式**:
```python
# 修改前
from config import MATERIAL_TYPE_MAPPINGS
carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1]
# 修改后
carrier_type_id = self.bioyond_config['material_type_mappings'][board_type][1]
```
---
## ✅ 验证结果
### 启动成功日志
```
✅ 从 JSON 配置加载 bioyond_config 成功
API Host: http://172.16.11.219:44388
HTTP Service: 172.16.11.206:8080
🔧 已设置 _disable_auto_http_service 标志,防止 HTTP 服务重复启动
✅ BioyondCellWorkstation 初始化完成
Loaded ResourceTreeSet with 1 trees, 1785 total nodes
```
### 功能验证
- ✅ 订单创建 (`create_orders_v2`)
- ✅ 质量比计算
- ✅ 物料转移 (`transfer_3_to_2_to_1`)
- ✅ HTTP 报送接收 (step_finish, sample_finish, order_finish)
- ✅ 等待机制 (`wait_for_order_finish`)
- ✅ 仓库 UUID 映射
- ✅ 物料类型映射
---
## 📚 相关文档
- **配置迁移经验**: `2026-01-13_JSON配置迁移经验.md`
- **任务清单**: `C:\Users\AndyXie\.gemini\antigravity\brain\...\task.md`
- **实施计划**: `C:\Users\AndyXie\.gemini\antigravity\brain\...\implementation_plan.md`
---
## ⚠️ 注意事项
### 其他工作站模块
以下文件仍在使用 `config.py` 全局变量(未包含在本次修改中):
- `reaction_station.py` - 使用 `API_CONFIG`
- `experiment.py` - 使用 `API_CONFIG`, `WORKFLOW_MAPPINGS`, `MATERIAL_TYPE_MAPPINGS`
- `dispensing_station.py` - 使用 `API_CONFIG`, `WAREHOUSE_MAPPING`
- `station.py` L176, L177, L529, L530 - 动态导入 `WAREHOUSE_MAPPING`
**建议**: 后续可以统一迁移这些模块到 JSON 配置。
### config.py 文件
`config.py` 文件已恢复但**不再被 bioyond_cell 使用**。可以:
- 保留作为其他模块的参考
- 或者完全删除(如果其他模块也迁移完成)
---
## 🚀 下一步建议
1. **清理调试代码** ✅ (已完成)
2. **提交代码到 Git**
3. **迁移其他工作站模块** (可选)
4. **更新文档和启动脚本**
---
**修改完成日期**: 2026-01-13
**系统状态**: ✅ 稳定运行

View File

@@ -1,157 +0,0 @@
# 批量出库 Excel 模板使用说明
**文件**: `outbound_template.xlsx`
**用途**: 配合 `auto_batch_outbound_from_xlsx()` 方法进行批量出库操作
**API 端点**: `/api/lims/storage/auto-batch-out-bound`
---
## 📋 Excel 列说明
| 列名 | 说明 | 示例 | 必填 |
|------|------|------|------|
| `locationId` | **库位 IDUUID** | `3a19da43-57b5-294f-d663-154a1cc32270` | ✅ 是 |
| `warehouseId` | **仓库 ID 或名称** | `配液站内试剂仓库` | ✅ 是 |
| `quantity` | **出库数量** | `1.0`, `2.0` | ✅ 是 |
| `x` | **X 坐标(库位横向位置)** | `1`, `2`, `3` | ✅ 是 |
| `y` | **Y 坐标(库位纵向位置)** | `1`, `2`, `3` | ✅ 是 |
| `z` | **Z 坐标(库位层数/高度)** | `1`, `2`, `3` | ✅ 是 |
| `备注说明` | 可选备注信息 | `配液站内试剂仓库-A01` | ❌ 否 |
### 📐 坐标说明
**x, y, z** 是库位在仓库内的**三维坐标**
```
仓库(例如 WH4
├── Z=1第1层/加样头面)
│ ├── X=1, Y=1位置 A
│ ├── X=2, Y=1位置 B
│ ├── X=3, Y=1位置 C
│ └── ...
└── Z=2第2层/原液瓶面)
├── X=1, Y=1位置 A
├── X=2, Y=1位置 B
└── ...
```
- **warehouseId**: 指定哪个仓库WH3, WH4, 配液站等)
- **x, y, z**: 在该仓库内的三维坐标
- **locationId**: 该坐标位置的唯一 UUID
### 🎯 起点与终点
**重要说明**:批量出库模板**只规定了出库的"起点"**(从哪里取物料),**没有指定"终点"**(放到哪里)。
```
出库流程:
起点Excel 指定) → 终点LIMS/工作流决定)
locationId, x, y, z → 由 LIMS 系统或当前工作流自动分配
```
**终点由以下方式确定:**
- **LIMS 系统自动分配**:根据当前任务自动规划目标位置
- **工作流预定义**:在创建出库任务时已绑定目标位置
- **暂存区**:默认放到出库暂存区,等待下一步操作
💡 **对比**:上料操作(`auto_feeding4to3`)则有 `targetWH` 参数可以指定目标仓库
---
## 🔍 如何获取 UUID
### 方法 1从配置文件获取
参考 `yibin_electrolyte_config.json` 中的 `warehouse_mapping`
```json
{
"warehouse_mapping": {
"配液站内试剂仓库": {
"site_uuids": {
"A01": "3a19da43-57b5-294f-d663-154a1cc32270",
"B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2",
"C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f"
}
},
"手动堆栈": {
"site_uuids": {
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe"
}
}
}
}
```
### 方法 2通过 API 查询
```python
material_info = hardware_interface.material_id_query(workflow_id)
locations = material_info.get("locations", [])
```
---
## 📝 填写示例
### 示例 1从配液站内试剂仓库出库
| locationId | warehouseId | quantity | x | y | z | 备注说明 |
|------------|-------------|----------|---|---|---|----------|
| `3a19da43-57b5-294f-d663-154a1cc32270` | 配液站内试剂仓库 | 1 | 1 | 1 | 1 | A01 位置 |
| `3a19da43-57b5-7394-5f49-54efe2c9bef2` | 配液站内试剂仓库 | 2 | 2 | 1 | 1 | B01 位置 |
### 示例 2从手动堆栈出库
| locationId | warehouseId | quantity | x | y | z | 备注说明 |
|------------|-------------|----------|---|---|---|----------|
| `3a19deae-2c7a-36f5-5e41-02c5b66feaea` | 手动堆栈 | 1 | 1 | 1 | 1 | A01 |
| `3a19deae-2c7a-dc6d-c41e-ef285d946cfe` | 手动堆栈 | 1 | 1 | 2 | 1 | A02 |
---
## 💻 使用方法
```python
from bioyond_cell_workstation import BioyondCellWorkstation
# 初始化工作站
workstation = BioyondCellWorkstation(config=config, deck=deck)
# 调用批量出库方法
result = workstation.auto_batch_outbound_from_xlsx(
xlsx_path="outbound_template.xlsx"
)
```
---
## ⚠️ 注意事项
1. **locationId 必须是有效的 UUID**,不能使用库位名称
2. **x, y, z 坐标必须与 locationId 对应**,表示该库位在仓库内的位置
3. **quantity 必须是数字**,可以是整数或浮点数
4. Excel 文件必须包含表头行
5. 空行会被自动跳过
6. 确保 UUID 与实际库位对应,否则 API 会报错
---
## 📚 相关文件
- **配置文件**: `yibin_electrolyte_config.json`
- **Python 代码**: `bioyond_cell_workstation.py` (L630-695)
- **生成脚本**: `create_outbound_template.py`
- **上料模板**: `material_template.xlsx`
---
## 🔄 重新生成模板
```bash
conda activate newunilab
python create_outbound_template.py
```

View File

@@ -9,7 +9,7 @@ from datetime import datetime, timezone
from unilabos.device_comms.rpc import BaseRequest from unilabos.device_comms.rpc import BaseRequest
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
import json import json
from unilabos.devices.workstation.bioyond_studio.config import LOCATION_MAPPING
class SimpleLogger: class SimpleLogger:
@@ -49,14 +49,6 @@ class BioyondV1RPC(BaseRequest):
self.config = config self.config = config
self.api_key = config["api_key"] self.api_key = config["api_key"]
self.host = config["api_host"] self.host = config["api_host"]
# 初始化 location_mapping
# 直接从 warehouse_mapping 构建,确保数据源所谓的单一和结构化
self.location_mapping = {}
warehouse_mapping = self.config.get("warehouse_mapping", {})
for warehouse_name, warehouse_config in warehouse_mapping.items():
if "site_uuids" in warehouse_config:
self.location_mapping.update(warehouse_config["site_uuids"])
self._logger = SimpleLogger() self._logger = SimpleLogger()
self.material_cache = {} self.material_cache = {}
self._load_material_cache() self._load_material_cache()
@@ -184,40 +176,7 @@ class BioyondV1RPC(BaseRequest):
return {} return {}
print(f"add material data: {response['data']}") print(f"add material data: {response['data']}")
return response.get("data", {})
# 自动更新缓存
data = response.get("data", {})
if data:
if isinstance(data, str):
# 如果返回的是字符串通常是ID
mat_id = data
name = params.get("name")
else:
# 如果返回的是字典尝试获取name和id
name = data.get("name") or params.get("name")
mat_id = data.get("id")
if name and mat_id:
self.material_cache[name] = mat_id
print(f"已自动更新缓存: {name} -> {mat_id}")
# 处理返回数据中的 details (如果有)
# 有些 API 返回结构可能直接包含 details或者在 data 字段中
details = data.get("details", []) if isinstance(data, dict) else []
if not details and isinstance(data, dict):
details = data.get("detail", [])
if details:
for detail in details:
d_name = detail.get("name")
# 尝试从不同字段获取 ID
d_id = detail.get("id") or detail.get("detailMaterialId")
if d_name and d_id:
self.material_cache[d_name] = d_id
print(f"已自动更新 detail 缓存: {d_name} -> {d_id}")
return data
def query_matial_type_id(self, data) -> list: def query_matial_type_id(self, data) -> list:
"""查找物料typeid""" """查找物料typeid"""
@@ -244,7 +203,7 @@ class BioyondV1RPC(BaseRequest):
params={ params={
"apiKey": self.api_key, "apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(), "requestTime": self.get_current_time_iso8601(),
"data": 0, "data": {},
}) })
if not response or response['code'] != 1: if not response or response['code'] != 1:
return [] return []
@@ -314,19 +273,11 @@ class BioyondV1RPC(BaseRequest):
if not response or response['code'] != 1: if not response or response['code'] != 1:
return {} return {}
# 自动更新缓存 - 移除被删除的物料
for name, mid in list(self.material_cache.items()):
if mid == material_id:
del self.material_cache[name]
print(f"已从缓存移除物料: {name}")
break
return response.get("data", {}) return response.get("data", {})
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict: def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
"""指定库位出库物料(通过库位名称)""" """指定库位出库物料(通过库位名称)"""
location_id = self.location_mapping.get(location_name, location_name) location_id = LOCATION_MAPPING.get(location_name, location_name)
params = { params = {
"materialId": material_id, "materialId": material_id,
@@ -1152,10 +1103,6 @@ class BioyondV1RPC(BaseRequest):
for detail_material in detail_materials: for detail_material in detail_materials:
detail_name = detail_material.get("name") detail_name = detail_material.get("name")
detail_id = detail_material.get("detailMaterialId") detail_id = detail_material.get("detailMaterialId")
if not detail_id:
# 尝试其他可能的字段
detail_id = detail_material.get("id")
if detail_name and detail_id: if detail_name and detail_id:
self.material_cache[detail_name] = detail_id self.material_cache[detail_name] = detail_id
print(f"加载detail材料: {detail_name} -> ID: {detail_id}") print(f"加载detail材料: {detail_name} -> ID: {detail_id}")
@@ -1176,14 +1123,6 @@ class BioyondV1RPC(BaseRequest):
print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}") print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}")
return material_id return material_id
# 如果缓存中没有,尝试刷新缓存
print(f"缓存中未找到材料 '{material_name_or_id}',尝试刷新缓存...")
self.refresh_material_cache()
if material_name_or_id in self.material_cache:
material_id = self.material_cache[material_name_or_id]
print(f"刷新缓存后找到材料: {material_name_or_id} -> ID: {material_id}")
return material_id
print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值") print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值")
return material_name_or_id return material_name_or_id

View File

@@ -0,0 +1,142 @@
# config.py
"""
配置文件 - 包含所有配置信息和映射关系
"""
# API配置
API_CONFIG = {
"api_key": "",
"api_host": ""
}
# 工作流映射配置
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': '反应器取出'
}
# 库位映射配置
WAREHOUSE_MAPPING = {
"粉末堆栈": {
"uuid": "",
"site_uuids": {
# 样品板
"A1": "3a14198e-6929-31f0-8a22-0f98f72260df",
"A2": "3a14198e-6929-4379-affa-9a2935c17f99",
"A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
"A4": "3a14198e-6929-5e99-2b79-80720f7cfb54",
"B1": "3a14198e-6929-f525-9a1b-1857552b28ee",
"B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
"B3": "3a14198e-6929-2d86-a468-602175a2b5aa",
"B4": "3a14198e-6929-1a98-ae57-e97660c489ad",
# 分装板
"C1": "3a14198e-6929-46fe-841e-03dd753f1e4a",
"C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
"C3": "3a14198e-6929-72ac-32ce-9b50245682b8",
"C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
"D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
"D2": "3a14198e-6929-dde1-fc78-34a84b71afdf",
"D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
"D4": "3a14198e-6929-7ac8-915a-fea51cb2e884"
}
},
"溶液堆栈": {
"uuid": "",
"site_uuids": {
"A1": "3a14198e-d724-e036-afdc-2ae39a7f3383",
"A2": "3a14198e-d724-afa4-fc82-0ac8a9016791",
"A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
"A4": "3a14198e-d724-df6d-5e32-5483b3cab583",
"B1": "3a14198e-d724-d818-6d4f-5725191a24b5",
"B2": "3a14198e-d724-be8a-5e0b-012675e195c6",
"B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
"B4": "3a14198e-d724-1e28-c885-574c3df468d0",
"C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
"C2": "3a14198e-d724-ab4e-48cb-817c3c146707",
"C3": "3a14198e-d724-7f18-1853-39d0c62e1d33",
"C4": "3a14198e-d724-28a2-a760-baa896f46b66",
"D1": "3a14198e-d724-d378-d266-2508a224a19f",
"D2": "3a14198e-d724-f56e-468b-0110a8feb36a",
"D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
"D4": "3a14198e-d724-0ddd-9654-f9352a421de9"
}
},
"试剂堆栈": {
"uuid": "",
"site_uuids": {
"A1": "3a14198c-c2cf-8b40-af28-b467808f1c36",
"A2": "3a14198c-c2d0-f3e7-871a-e470d144296f",
"A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094",
"A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
"B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8",
"B2": "3a14198c-c2d0-1559-105d-0ea30682cab4",
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
}
}
}
# 物料类型配置
MATERIAL_TYPE_MAPPINGS = {
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
}
# 步骤参数配置各工作流的步骤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": ""
}
}
LOCATION_MAPPING = {}
ACTION_NAMES = {}
HTTP_SERVICE_CONFIG = {}

View File

@@ -1,329 +0,0 @@
# config.py
"""
Bioyond工作站配置文件
包含API配置、工作流映射、物料类型映射、仓库库位映射等所有配置信息
"""
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
# ============================================================================
# 基础配置
# ============================================================================
# API配置
API_CONFIG = {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44402"
}
# HTTP 报送服务配置
HTTP_SERVICE_CONFIG = {
"http_service_host": "127.0.0.1", # 监听地址
"http_service_port": 8080, # 监听端口
}
# Deck配置 - 反应站工作台配置
DECK_CONFIG = BIOYOND_PolymerReactionStation_Deck(setup=True)
# ============================================================================
# 工作流配置
# ============================================================================
# 工作流ID映射
WORKFLOW_MAPPINGS = {
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6",
"liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a",
}
# 工作流名称到显示名称的映射
WORKFLOW_TO_SECTION_MAP = {
'reactor_taken_in': '反应器放入',
'reactor_taken_out': '反应器取出',
'Solid_feeding_vials': '固体投料-小瓶',
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
'Liquid_feeding_solvents': '液体投料-溶剂',
'Liquid_feeding(titration)': '液体投料-滴定',
'liquid_feeding_beaker': '液体投料-烧杯',
'Drip_back': '液体回滴'
}
# 工作流步骤ID配置
WORKFLOW_STEP_IDS = {
"reactor_taken_in": {
"config": "60a06f85-c5b3-29eb-180f-4f62dd7e2154"
},
"liquid_feeding_beaker": {
"liquid": "6808cda7-fee7-4092-97f0-5f9c2ffa60e3",
"observe": "1753c0de-dffc-4ee6-8458-805a2e227362"
},
"liquid_feeding_vials_non_titration": {
"liquid": "62ea6e95-3d5d-43db-bc1e-9a1802673861",
"observe": "3a167d99-6172-b67b-5f22-a7892197142e"
},
"liquid_feeding_solvents": {
"liquid": "1fcea355-2545-462b-b727-350b69a313bf",
"observe": "0553dfb3-9ac5-4ace-8e00-2f11029919a8"
},
"solid_feeding_vials": {
"feeding": "f7ae7448-4f20-4c1d-8096-df6fbadd787a",
"observe": "263c7ed5-7277-426b-bdff-d6fbf77bcc05"
},
"liquid_feeding_titration": {
"liquid": "a00ec41b-e666-4422-9c20-bfcd3cd15c54",
"observe": "ac738ff6-4c58-4155-87b1-d6f65a2c9ab5"
},
"drip_back": {
"liquid": "371be86a-ab77-4769-83e5-54580547c48a",
"observe": "ce024b9d-bd20-47b8-9f78-ca5ce7f44cf1"
}
}
# 工作流动作名称配置
ACTION_NAMES = {
"reactor_taken_in": {
"config": "通量-配置",
"stirring": "反应模块-开始搅拌"
},
"solid_feeding_vials": {
"feeding": "粉末加样模块-投料",
"observe": "反应模块-观察搅拌结果"
},
"liquid_feeding_vials_non_titration": {
"liquid": "稀释液瓶加液位-液体投料",
"observe": "反应模块-滴定结果观察"
},
"liquid_feeding_solvents": {
"liquid": "试剂AB放置位-试剂吸液分液",
"observe": "反应模块-观察搅拌结果"
},
"liquid_feeding_titration": {
"liquid": "稀释液瓶加液位-稀释液吸液分液",
"observe": "反应模块-滴定结果观察"
},
"liquid_feeding_beaker": {
"liquid": "烧杯溶液放置位-烧杯吸液分液",
"observe": "反应模块-观察搅拌结果"
},
"drip_back": {
"liquid": "试剂AB放置位-试剂吸液分液",
"observe": "反应模块-向下滴定结果观察"
}
}
# ============================================================================
# 仓库配置
# ============================================================================
# 说明:
# - 出库和入库操作都需要UUID
WAREHOUSE_MAPPING = {
# ========== 反应站仓库 ==========
# 堆栈1左 - 反应站左侧堆栈 (4行×4列=16个库位, A01D04)
"堆栈1左": {
"uuid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
"site_uuids": {
"A01": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3",
"A02": "3a14aa17-0d49-4bc5-8836-517b75473f5f",
"A03": "3a14aa17-0d49-c2bc-6222-5cee8d2d94f8",
"A04": "3a14aa17-0d49-3ce2-8e9a-008c38d116fb",
"B01": "3a14aa17-0d49-f49c-6b66-b27f185a3b32",
"B02": "3a14aa17-0d49-cf46-df85-a979c9c9920c",
"B03": "3a14aa17-0d49-7698-4a23-f7ffb7d48ba3",
"B04": "3a14aa17-0d49-1231-99be-d5870e6478e9",
"C01": "3a14aa17-0d49-be34-6fae-4aed9d48b70b",
"C02": "3a14aa17-0d49-11d7-0897-34921dcf6b7c",
"C03": "3a14aa17-0d49-9840-0bd5-9c63c1bb2c29",
"C04": "3a14aa17-0d49-8335-3bff-01da69ea4911",
"D01": "3a14aa17-0d49-2bea-c8e5-2b32094935d5",
"D02": "3a14aa17-0d49-cff4-e9e8-5f5f0bc1ef32",
"D03": "3a14aa17-0d49-4948-cb0a-78f30d1ca9b8",
"D04": "3a14aa17-0d49-fd2f-9dfb-a29b11e84099",
},
},
# 堆栈1右 - 反应站右侧堆栈 (4行×4列=16个库位, A05D08)
"堆栈1右": {
"uuid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
"site_uuids": {
"A05": "3a14aa17-0d49-2c61-edc8-72a8ca7192dd",
"A06": "3a14aa17-0d49-60c8-2b00-40b17198f397",
"A07": "3a14aa17-0d49-ec5b-0b75-634dce8eed25",
"A08": "3a14aa17-0d49-3ec9-55b3-f3189c4ec53d",
"B05": "3a14aa17-0d49-6a4e-abcf-4c113eaaeaad",
"B06": "3a14aa17-0d49-e3f6-2dd6-28c2e8194fbe",
"B07": "3a14aa17-0d49-11a6-b861-ee895121bf52",
"B08": "3a14aa17-0d49-9c7d-1145-d554a6e482f0",
"C05": "3a14aa17-0d49-45c4-7a34-5105bc3e2368",
"C06": "3a14aa17-0d49-867e-39ab-31b3fe9014be",
"C07": "3a14aa17-0d49-ec56-c4b4-39fd9b2131e7",
"C08": "3a14aa17-0d49-1128-d7d9-ffb1231c98c0",
"D05": "3a14aa17-0d49-e843-f961-ea173326a14b",
"D06": "3a14aa17-0d49-4d26-a985-f188359c4f8b",
"D07": "3a14aa17-0d49-223a-b520-bc092bb42fe0",
"D08": "3a14aa17-0d49-4fa3-401a-6a444e1cca22",
},
},
# 站内试剂存放堆栈
"站内试剂存放堆栈": {
"uuid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
"site_uuids": {
"A01": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5",
"A02": "3a14aa3b-9fab-ca72-febc-b7c304476c78"
}
},
# 测量小瓶仓库(测密度)
"测量小瓶仓库": {
"uuid": "3a15012f-705b-c0de-3f9e-950c205f9921",
"site_uuids": {
"A01": "3a15012f-705e-0524-3161-c523b5aebc97",
"A02": "3a15012f-705e-7cd1-32ab-ad4fd1ab75c8",
"A03": "3a15012f-705e-a5d6-edac-bdbfec236260",
"B01": "3a15012f-705e-e0ee-80e0-10a6b3fc500d",
"B02": "3a15012f-705e-e499-180d-de06d60d0b21",
"B03": "3a15012f-705e-eff6-63f1-09f742096b26"
}
},
# 站内Tip盒堆栈 - 用于存放枪头盒 (耗材)
"站内Tip盒堆栈": {
"uuid": "3a14aa3a-2d3c-b5c1-9ddf-7c4a957d459a",
"site_uuids": {
"A01": "3a14aa3a-2d3d-e700-411a-0ddf85e1f18a",
"A02": "3a14aa3a-2d3d-a7ce-099a-d5632fdafa24",
"A03": "3a14aa3a-2d3d-bdf6-a702-c60b38b08501",
"B01": "3a14aa3a-2d3d-d704-f076-2a8d5bc72cb8",
"B02": "3a14aa3a-2d3d-c350-2526-0778d173a5ac",
"B03": "3a14aa3a-2d3d-bc38-b356-f0de2e44e0c7"
}
},
# ========== 配液站仓库 ==========
"粉末堆栈": {
"uuid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0",
"site_uuids": {
"A01": "3a14198e-6929-31f0-8a22-0f98f72260df",
"A02": "3a14198e-6929-4379-affa-9a2935c17f99",
"A03": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
"A04": "3a14198e-6929-5e99-2b79-80720f7cfb54",
"B01": "3a14198e-6929-f525-9a1b-1857552b28ee",
"B02": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
"B03": "3a14198e-6929-2d86-a468-602175a2b5aa",
"B04": "3a14198e-6929-1a98-ae57-e97660c489ad",
"C01": "3a14198e-6929-46fe-841e-03dd753f1e4a",
"C02": "3a14198e-6929-72ac-32ce-9b50245682b8",
"C03": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
"C04": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
"D01": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
"D02": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
"D03": "3a14198e-6929-dde1-fc78-34a84b71afdf",
"D04": "3a14198e-6929-7ac8-915a-fea51cb2e884"
}
},
"溶液堆栈": {
"uuid": "3a14198e-d723-2c13-7d12-50143e190a23",
"site_uuids": {
"A01": "3a14198e-d724-e036-afdc-2ae39a7f3383",
"A02": "3a14198e-d724-d818-6d4f-5725191a24b5",
"A03": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
"A04": "3a14198e-d724-d378-d266-2508a224a19f",
"B01": "3a14198e-d724-afa4-fc82-0ac8a9016791",
"B02": "3a14198e-d724-be8a-5e0b-012675e195c6",
"B03": "3a14198e-d724-ab4e-48cb-817c3c146707",
"B04": "3a14198e-d724-f56e-468b-0110a8feb36a",
"C01": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
"C02": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
"C03": "3a14198e-d724-7f18-1853-39d0c62e1d33",
"C04": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
"D01": "3a14198e-d724-df6d-5e32-5483b3cab583",
"D02": "3a14198e-d724-1e28-c885-574c3df468d0",
"D03": "3a14198e-d724-28a2-a760-baa896f46b66",
"D04": "3a14198e-d724-0ddd-9654-f9352a421de9"
}
},
"试剂堆栈": {
"uuid": "3a14198c-c2cc-0290-e086-44a428fba248",
"site_uuids": {
"A01": "3a14198c-c2cf-8b40-af28-b467808f1c36", # x=1, y=1, code=0001-0001
"A02": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094", # x=1, y=2, code=0001-0002
"A03": "3a14198c-c2d0-354f-39ad-642e1a72fcb8", # x=1, y=3, code=0001-0003
"A04": "3a14198c-c2d0-725e-523d-34c037ac2440", # x=1, y=4, code=0001-0004
"B01": "3a14198c-c2d0-f3e7-871a-e470d144296f", # x=2, y=1, code=0001-0005
"B02": "3a14198c-c2d0-2070-efc8-44e245f10c6f", # x=2, y=2, code=0001-0006
"B03": "3a14198c-c2d0-1559-105d-0ea30682cab4", # x=2, y=3, code=0001-0007
"B04": "3a14198c-c2d0-efce-0939-69ca5a7dfd39" # x=2, y=4, code=0001-0008
}
}
}
# ============================================================================
# 物料类型配置
# ============================================================================
# 说明:
# - 格式: PyLabRobot资源类型名称 → Bioyond系统typeId的UUID
# - 这个映射基于 resource.model 属性 (不是显示名称!)
# - UUID为空表示该类型暂未在Bioyond系统中定义
MATERIAL_TYPE_MAPPINGS = {
# ================================================配液站资源============================================================
# ==================================================样品===============================================================
"BIOYOND_PolymerStation_1FlaskCarrier": ("烧杯", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), # 配液站-样品-烧杯
"BIOYOND_PolymerStation_1BottleCarrier": ("试剂瓶", "3a14196b-8bcf-a460-4f74-23f21ca79e72"), # 配液站-样品-试剂瓶
"BIOYOND_PolymerStation_6StockCarrier": ("分装板", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), # 配液站-样品-分装板
"BIOYOND_PolymerStation_Liquid_Vial": ("10%分装小瓶", "3a14196c-76be-2279-4e22-7310d69aed68"), # 配液站-样品-分装板-第一排小瓶
"BIOYOND_PolymerStation_Solid_Vial": ("90%分装小瓶", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), # 配液站-样品-分装板-第二排小瓶
# ==================================================试剂===============================================================
"BIOYOND_PolymerStation_8StockCarrier": ("样品板", "3a14196e-b7a0-a5da-1931-35f3000281e9"), # 配液站-试剂-样品板8孔
"BIOYOND_PolymerStation_Solid_Stock": ("样品瓶", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), # 配液站-试剂-样品板-样品瓶
}
# ============================================================================
# 动态生成的库位UUID映射从WAREHOUSE_MAPPING中提取
# ============================================================================
LOCATION_MAPPING = {}
for warehouse_name, warehouse_config in WAREHOUSE_MAPPING.items():
if "site_uuids" in warehouse_config:
LOCATION_MAPPING.update(warehouse_config["site_uuids"])
# ============================================================================
# 物料默认参数配置
# ============================================================================
# 说明:
# - 为特定物料名称自动添加默认参数(如密度、分子量、单位等)
# - 格式: 物料名称 → {参数字典}
# - 在创建或更新物料时,会自动合并这些参数到 Parameters 字段
# - unit: 物料的计量单位(会用于 unit 字段)
# - density/densityUnit: 密度信息(会添加到 Parameters 中)
MATERIAL_DEFAULT_PARAMETERS = {
# 溶剂类
"NMP": {
"unit": "毫升",
"density": "1.03",
"densityUnit": "g/mL",
"description": "N-甲基吡咯烷酮 (N-Methyl-2-pyrrolidone)"
},
# 可以继续添加其他物料...
}
# ============================================================================
# 物料类型默认参数配置
# ============================================================================
# 说明:
# - 为特定物料类型UUID自动添加默认参数
# - 格式: Bioyond类型UUID → {参数字典}
# - 优先级低于按名称匹配的配置
MATERIAL_TYPE_PARAMETERS = {
# 示例:
# "3a14196b-24f2-ca49-9081-0cab8021bf1a": { # 烧杯
# "unit": "个"
# }
}

View File

@@ -4,8 +4,7 @@ import time
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from typing_extensions import TypedDict from typing_extensions import TypedDict
import requests import requests
import pint from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
@@ -26,89 +25,13 @@ class ComputeExperimentDesignReturn(TypedDict):
class BioyondDispensingStation(BioyondWorkstation): class BioyondDispensingStation(BioyondWorkstation):
def __init__( def __init__(
self, self,
config: dict = None, config,
deck=None, # 桌子
protocol_type=None, deck,
*args,
**kwargs, **kwargs,
): ):
"""初始化配液站 super().__init__(config, deck, *args, **kwargs)
Args:
config: 配置字典,应包含material_type_mappings等配置
deck: Deck对象
protocol_type: 协议类型(由ROS系统传递,此处忽略)
**kwargs: 其他可能的参数
"""
if config is None:
config = {}
# 将 kwargs 合并到 config 中 (处理扁平化配置如 api_key)
config.update(kwargs)
if deck is None and config:
deck = config.get('deck')
# 🔧 修复: 确保 Deck 上的 warehouses 具有正确的 UUID (必须在 super().__init__ 之前执行,因为父类会触发同步)
# 从配置中读取 warehouse_mapping并应用到实际的 deck 资源上
if config and "warehouse_mapping" in config and deck:
warehouse_mapping = config["warehouse_mapping"]
print(f"正在根据配置更新 Deck warehouse UUIDs... (共有 {len(warehouse_mapping)} 个配置)")
user_deck = deck
# 初始化 warehouses 字典
if not hasattr(user_deck, "warehouses") or user_deck.warehouses is None:
user_deck.warehouses = {}
# 1. 尝试从 children 中查找匹配的资源
for child in user_deck.children:
# 简单判断: 如果名字在 mapping 中,就认为是 warehouse
if child.name in warehouse_mapping:
user_deck.warehouses[child.name] = child
print(f" - 从子资源中找到 warehouse: {child.name}")
# 2. 如果还是没找到,且 Deck 类有 setup 方法,尝试调用 setup (针对 Deck 对象正确但未初始化的情况)
if not user_deck.warehouses and hasattr(user_deck, "setup"):
print(" - 尝试调用 deck.setup() 初始化仓库...")
try:
user_deck.setup()
# setup 后重新检查
if hasattr(user_deck, "warehouses") and user_deck.warehouses:
print(f" - setup() 成功,找到 {len(user_deck.warehouses)} 个仓库")
except Exception as e:
print(f" - 调用 setup() 失败: {e}")
# 3. 如果仍然为空,可能需要手动创建 (仅针对特定已知的 Deck 类型进行补救,这里暂时只打印警告)
if not user_deck.warehouses:
print(" - ⚠️ 仍然无法找到任何 warehouse 资源!")
for wh_name, wh_config in warehouse_mapping.items():
target_uuid = wh_config.get("uuid")
# 尝试在 deck.warehouses 中查找
wh_resource = None
if hasattr(user_deck, "warehouses") and wh_name in user_deck.warehouses:
wh_resource = user_deck.warehouses[wh_name]
# 如果没找到,尝试在所有子资源中查找
if not wh_resource:
wh_resource = user_deck.get_resource(wh_name)
if wh_resource:
if target_uuid:
current_uuid = getattr(wh_resource, "uuid", None)
print(f"✅ 更新仓库 '{wh_name}' UUID: {current_uuid} -> {target_uuid}")
# 动态添加 uuid 属性
wh_resource.uuid = target_uuid
# 同时也确保 category 正确,避免 graphio 识别错误
# wh_resource.category = "warehouse"
else:
print(f"⚠️ 仓库 '{wh_name}' 在配置中没有 UUID")
else:
print(f"❌ 在 Deck 中未找到配置的仓库: '{wh_name}'")
super().__init__(bioyond_config=config, deck=deck)
# self.config = config # self.config = config
# self.api_key = config["api_key"] # self.api_key = config["api_key"]
# self.host = config["api_host"] # self.host = config["api_host"]
@@ -120,41 +43,6 @@ class BioyondDispensingStation(BioyondWorkstation):
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}} # 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
self.order_completion_status = {} self.order_completion_status = {}
# 初始化 pint 单位注册表
self.ureg = pint.UnitRegistry()
# 化合物信息
self.compound_info = {
"MolWt": {
"MDA": 108.14 * self.ureg.g / self.ureg.mol,
"TDA": 122.16 * self.ureg.g / self.ureg.mol,
"PAPP": 521.62 * self.ureg.g / self.ureg.mol,
"BTDA": 322.23 * self.ureg.g / self.ureg.mol,
"BPDA": 294.22 * self.ureg.g / self.ureg.mol,
"6FAP": 366.26 * self.ureg.g / self.ureg.mol,
"PMDA": 218.12 * self.ureg.g / self.ureg.mol,
"MPDA": 108.14 * self.ureg.g / self.ureg.mol,
"SIDA": 248.51 * self.ureg.g / self.ureg.mol,
"ODA": 200.236 * self.ureg.g / self.ureg.mol,
"4,4'-ODA": 200.236 * self.ureg.g / self.ureg.mol,
"134": 292.34 * self.ureg.g / self.ureg.mol,
},
"FuncGroup": {
"MDA": "Amine",
"TDA": "Amine",
"PAPP": "Amine",
"BTDA": "Anhydride",
"BPDA": "Anhydride",
"6FAP": "Amine",
"MPDA": "Amine",
"SIDA": "Amine",
"PMDA": "Anhydride",
"ODA": "Amine",
"4,4'-ODA": "Amine",
"134": "Amine",
}
}
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]: def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
"""项目接口通用POST调用 """项目接口通用POST调用
@@ -166,7 +54,7 @@ class BioyondDispensingStation(BioyondWorkstation):
dict: 服务端响应失败时返回 {code:0,message,...} dict: 服务端响应失败时返回 {code:0,message,...}
""" """
request_data = { request_data = {
"apiKey": self.bioyond_config["api_key"], "apiKey": API_CONFIG["api_key"],
"requestTime": self.hardware_interface.get_current_time_iso8601(), "requestTime": self.hardware_interface.get_current_time_iso8601(),
"data": data "data": data
} }
@@ -197,7 +85,7 @@ class BioyondDispensingStation(BioyondWorkstation):
dict: 服务端响应失败时返回 {code:0,message,...} dict: 服务端响应失败时返回 {code:0,message,...}
""" """
request_data = { request_data = {
"apiKey": self.bioyond_config["api_key"], "apiKey": API_CONFIG["api_key"],
"requestTime": self.hardware_interface.get_current_time_iso8601(), "requestTime": self.hardware_interface.get_current_time_iso8601(),
"data": data "data": data
} }
@@ -230,22 +118,20 @@ class BioyondDispensingStation(BioyondWorkstation):
ratio = json.loads(ratio) ratio = json.loads(ratio)
except Exception: except Exception:
ratio = {} ratio = {}
root = str(Path(__file__).resolve().parents[3])
if root not in sys.path:
sys.path.append(root)
try:
mod = importlib.import_module("tem.compute")
except Exception as e:
raise BioyondException(f"无法导入计算模块: {e}")
try: try:
wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent
mt = float(m_tot) if isinstance(m_tot, str) else m_tot mt = float(m_tot) if isinstance(m_tot, str) else m_tot
tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent
except Exception as e: except Exception as e:
raise BioyondException(f"参数解析失败: {e}") raise BioyondException(f"参数解析失败: {e}")
res = mod.generate_experiment_design(ratio=ratio, wt_percent=wp, m_tot=mt, titration_percent=tp)
# 2. 调用内部计算方法
res = self._generate_experiment_design(
ratio=ratio,
wt_percent=wp,
m_tot=mt,
titration_percent=tp
)
# 3. 构造返回结果
out = { out = {
"solutions": res.get("solutions", []), "solutions": res.get("solutions", []),
"titration": res.get("titration", {}), "titration": res.get("titration", {}),
@@ -254,248 +140,11 @@ class BioyondDispensingStation(BioyondWorkstation):
"return_info": json.dumps(res, ensure_ascii=False) "return_info": json.dumps(res, ensure_ascii=False)
} }
return out return out
except BioyondException: except BioyondException:
raise raise
except Exception as e: except Exception as e:
raise BioyondException(str(e)) raise BioyondException(str(e))
def _generate_experiment_design(
self,
ratio: dict,
wt_percent: float = 0.25,
m_tot: float = 70,
titration_percent: float = 0.03,
) -> dict:
"""内部方法:生成实验设计
根据FuncGroup自动区分二胺和二酐每种二胺单独配溶液严格按照ratio顺序投料
参数:
ratio: 化合物配比字典格式: {"compound_name": ratio_value}
wt_percent: 固体重量百分比
m_tot: 反应混合物总质量(g)
titration_percent: 滴定溶液百分比
返回:
包含实验设计详细参数的字典
"""
# 溶剂密度
ρ_solvent = 1.03 * self.ureg.g / self.ureg.ml
# 二酐溶解度
solubility = 0.02 * self.ureg.g / self.ureg.ml
# 投入固体时最小溶剂体积
V_min = 30 * self.ureg.ml
m_tot = m_tot * self.ureg.g
# 保持ratio中的顺序
compound_names = list(ratio.keys())
compound_ratios = list(ratio.values())
# 验证所有化合物是否在 compound_info 中定义
undefined_compounds = [name for name in compound_names if name not in self.compound_info["MolWt"]]
if undefined_compounds:
available = list(self.compound_info["MolWt"].keys())
raise ValueError(
f"以下化合物未在 compound_info 中定义: {undefined_compounds}"
f"可用的化合物: {available}"
)
# 获取各化合物的分子量和官能团类型
molecular_weights = [self.compound_info["MolWt"][name] for name in compound_names]
func_groups = [self.compound_info["FuncGroup"][name] for name in compound_names]
# 记录化合物信息用于调试
self.hardware_interface._logger.info(f"化合物名称: {compound_names}")
self.hardware_interface._logger.info(f"官能团类型: {func_groups}")
# 按原始顺序分离二胺和二酐
ordered_compounds = list(zip(compound_names, compound_ratios, molecular_weights, func_groups))
diamine_compounds = [(name, ratio_val, mw, i) for i, (name, ratio_val, mw, fg) in enumerate(ordered_compounds) if fg == "Amine"]
anhydride_compounds = [(name, ratio_val, mw, i) for i, (name, ratio_val, mw, fg) in enumerate(ordered_compounds) if fg == "Anhydride"]
if not diamine_compounds or not anhydride_compounds:
raise ValueError(
f"需要同时包含二胺(Amine)和二酐(Anhydride)化合物。"
f"当前二胺: {[c[0] for c in diamine_compounds]}, "
f"当前二酐: {[c[0] for c in anhydride_compounds]}"
)
# 计算加权平均分子量 (基于摩尔比)
total_molar_ratio = sum(compound_ratios)
weighted_molecular_weight = sum(ratio_val * mw for ratio_val, mw in zip(compound_ratios, molecular_weights))
# 取最后一个二酐用于滴定
titration_anhydride = anhydride_compounds[-1]
solid_anhydrides = anhydride_compounds[:-1] if len(anhydride_compounds) > 1 else []
# 二胺溶液配制参数 - 每种二胺单独配制
diamine_solutions = []
total_diamine_volume = 0 * self.ureg.ml
# 计算反应物的总摩尔量
n_reactant = m_tot * wt_percent / weighted_molecular_weight
for name, ratio_val, mw, order_index in diamine_compounds:
# 跳过 SIDA
if name == "SIDA":
continue
# 计算该二胺需要的摩尔数
n_diamine_needed = n_reactant * ratio_val
# 二胺溶液配制参数 (每种二胺固定配制参数)
m_diamine_solid = 5.0 * self.ureg.g # 每种二胺固体质量
V_solvent_for_this = 20 * self.ureg.ml # 每种二胺溶剂体积
m_solvent_for_this = ρ_solvent * V_solvent_for_this
# 计算该二胺溶液的浓度
c_diamine = (m_diamine_solid / mw) / V_solvent_for_this
# 计算需要移取的溶液体积
V_diamine_needed = n_diamine_needed / c_diamine
diamine_solutions.append({
"name": name,
"order": order_index,
"solid_mass": m_diamine_solid.magnitude,
"solvent_volume": V_solvent_for_this.magnitude,
"concentration": c_diamine.magnitude,
"volume_needed": V_diamine_needed.magnitude,
"molar_ratio": ratio_val
})
total_diamine_volume += V_diamine_needed
# 按原始顺序排序
diamine_solutions.sort(key=lambda x: x["order"])
# 计算滴定二酐的质量
titration_name, titration_ratio, titration_mw, _ = titration_anhydride
m_titration_anhydride = n_reactant * titration_ratio * titration_mw
m_titration_90 = m_titration_anhydride * (1 - titration_percent)
m_titration_10 = m_titration_anhydride * titration_percent
# 计算其他固体二酐的质量 (按顺序)
solid_anhydride_masses = []
for name, ratio_val, mw, order_index in solid_anhydrides:
mass = n_reactant * ratio_val * mw
solid_anhydride_masses.append({
"name": name,
"order": order_index,
"mass": mass.magnitude,
"molar_ratio": ratio_val
})
# 按原始顺序排序
solid_anhydride_masses.sort(key=lambda x: x["order"])
# 计算溶剂用量
total_diamine_solution_mass = sum(
sol["volume_needed"] * ρ_solvent for sol in diamine_solutions
) * self.ureg.ml
# 预估滴定溶剂量、计算补加溶剂量
m_solvent_titration = m_titration_10 / solubility * ρ_solvent
m_solvent_add = m_tot * (1 - wt_percent) - total_diamine_solution_mass - m_solvent_titration
# 检查最小溶剂体积要求
total_liquid_volume = (total_diamine_solution_mass + m_solvent_add) / ρ_solvent
m_tot_min = V_min / total_liquid_volume * m_tot
# 如果需要,按比例放大
scale_factor = 1.0
if m_tot_min > m_tot:
scale_factor = (m_tot_min / m_tot).magnitude
m_titration_90 *= scale_factor
m_titration_10 *= scale_factor
m_solvent_add *= scale_factor
m_solvent_titration *= scale_factor
# 更新二胺溶液用量
for sol in diamine_solutions:
sol["volume_needed"] *= scale_factor
# 更新固体二酐用量
for anhydride in solid_anhydride_masses:
anhydride["mass"] *= scale_factor
m_tot = m_tot_min
# 生成投料顺序
feeding_order = []
# 1. 固体二酐 (按顺序)
for anhydride in solid_anhydride_masses:
feeding_order.append({
"step": len(feeding_order) + 1,
"type": "solid_anhydride",
"name": anhydride["name"],
"amount": anhydride["mass"],
"order": anhydride["order"]
})
# 2. 二胺溶液 (按顺序)
for sol in diamine_solutions:
feeding_order.append({
"step": len(feeding_order) + 1,
"type": "diamine_solution",
"name": sol["name"],
"amount": sol["volume_needed"],
"order": sol["order"]
})
# 3. 主要二酐粉末
feeding_order.append({
"step": len(feeding_order) + 1,
"type": "main_anhydride",
"name": titration_name,
"amount": m_titration_90.magnitude,
"order": titration_anhydride[3]
})
# 4. 补加溶剂
if m_solvent_add > 0:
feeding_order.append({
"step": len(feeding_order) + 1,
"type": "additional_solvent",
"name": "溶剂",
"amount": m_solvent_add.magnitude,
"order": 999
})
# 5. 滴定二酐溶液
feeding_order.append({
"step": len(feeding_order) + 1,
"type": "titration_anhydride",
"name": f"{titration_name} 滴定液",
"amount": m_titration_10.magnitude,
"titration_solvent": m_solvent_titration.magnitude,
"order": titration_anhydride[3]
})
# 返回实验设计结果
results = {
"total_mass": m_tot.magnitude,
"scale_factor": scale_factor,
"solutions": diamine_solutions,
"solids": solid_anhydride_masses,
"titration": {
"name": titration_name,
"main_portion": m_titration_90.magnitude,
"titration_portion": m_titration_10.magnitude,
"titration_solvent": m_solvent_titration.magnitude,
},
"solvents": {
"additional_solvent": m_solvent_add.magnitude,
"total_liquid_volume": total_liquid_volume.magnitude
},
"feeding_order": feeding_order,
"minimum_required_mass": m_tot_min.magnitude
}
return results
# 90%10%小瓶投料任务创建方法 # 90%10%小瓶投料任务创建方法
def create_90_10_vial_feeding_task(self, def create_90_10_vial_feeding_task(self,
order_name: str = None, order_name: str = None,
@@ -1312,108 +961,6 @@ class BioyondDispensingStation(BioyondWorkstation):
'actualVolume': actual_volume 'actualVolume': actual_volume
} }
def _simplify_report(self, report) -> Dict[str, Any]:
"""简化实验报告,只保留关键信息,去除冗余的工作流参数"""
if not isinstance(report, dict):
return report
data = report.get('data', {})
if not isinstance(data, dict):
return report
# 提取关键信息
simplified = {
'name': data.get('name'),
'code': data.get('code'),
'requester': data.get('requester'),
'workflowName': data.get('workflowName'),
'workflowStep': data.get('workflowStep'),
'requestTime': data.get('requestTime'),
'startPreparationTime': data.get('startPreparationTime'),
'completeTime': data.get('completeTime'),
'useTime': data.get('useTime'),
'status': data.get('status'),
'statusName': data.get('statusName'),
}
# 提取物料信息(简化版)
pre_intakes = data.get('preIntakes', [])
if pre_intakes and isinstance(pre_intakes, list):
first_intake = pre_intakes[0]
sample_materials = first_intake.get('sampleMaterials', [])
# 简化物料信息
simplified_materials = []
for material in sample_materials:
if isinstance(material, dict):
mat_info = {
'materialName': material.get('materialName'),
'materialTypeName': material.get('materialTypeName'),
'materialCode': material.get('materialCode'),
'materialLocation': material.get('materialLocation'),
}
# 解析parameters中的关键信息如密度、加料历史等
params_str = material.get('parameters', '{}')
try:
params = json.loads(params_str) if isinstance(params_str, str) else params_str
if isinstance(params, dict):
# 只保留关键参数
if 'density' in params:
mat_info['density'] = params['density']
if 'feedingHistory' in params:
mat_info['feedingHistory'] = params['feedingHistory']
if 'liquidVolume' in params:
mat_info['liquidVolume'] = params['liquidVolume']
if 'm_diamine_tot' in params:
mat_info['m_diamine_tot'] = params['m_diamine_tot']
if 'wt_diamine' in params:
mat_info['wt_diamine'] = params['wt_diamine']
except:
pass
simplified_materials.append(mat_info)
simplified['sampleMaterials'] = simplified_materials
# 提取extraProperties中的实际值
extra_props = first_intake.get('extraProperties', {})
if isinstance(extra_props, dict):
simplified_extra = {}
for key, value in extra_props.items():
try:
parsed_value = json.loads(value) if isinstance(value, str) else value
simplified_extra[key] = parsed_value
except:
simplified_extra[key] = value
simplified['extraProperties'] = simplified_extra
return {
'data': simplified,
'code': report.get('code'),
'message': report.get('message'),
'timestamp': report.get('timestamp')
}
def scheduler_start(self) -> dict:
"""启动调度器 - 启动Bioyond工作站的任务调度器开始执行队列中的任务
Returns:
dict: 包含return_info的字典return_info为整型(1=成功)
Raises:
BioyondException: 调度器启动失败时抛出异常
"""
result = self.hardware_interface.scheduler_start()
self.hardware_interface._logger.info(f"调度器启动结果: {result}")
if result != 1:
error_msg = "启动调度器失败: 有未处理错误调度无法启动。请检查Bioyond系统状态。"
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
return {"return_info": result}
# 等待多个任务完成并获取实验报告 # 等待多个任务完成并获取实验报告
def wait_for_multiple_orders_and_get_reports(self, def wait_for_multiple_orders_and_get_reports(self,
batch_create_result: str = None, batch_create_result: str = None,
@@ -1455,12 +1002,7 @@ class BioyondDispensingStation(BioyondWorkstation):
# 验证batch_create_result参数 # 验证batch_create_result参数
if not batch_create_result or batch_create_result == "": if not batch_create_result or batch_create_result == "":
raise BioyondException( raise BioyondException("batch_create_result参数为空请确保从batch_create节点正确连接handle")
"batch_create_result参数为空请确保:\n"
"1. batch_create节点与wait节点之间正确连接了handle\n"
"2. batch_create节点成功执行并返回了结果\n"
"3. 检查上游batch_create任务是否成功创建了订单"
)
# 解析batch_create_result JSON对象 # 解析batch_create_result JSON对象
try: try:
@@ -1489,17 +1031,7 @@ class BioyondDispensingStation(BioyondWorkstation):
# 验证提取的数据 # 验证提取的数据
if not order_codes: if not order_codes:
self.hardware_interface._logger.error( raise BioyondException("batch_create_result中未找到order_codes字段或为空")
f"batch_create任务未生成任何订单。batch_create_result内容: {batch_create_result}"
)
raise BioyondException(
"batch_create_result中未找到order_codes或为空。\n"
"可能的原因:\n"
"1. batch_create任务执行失败检查任务是否报错\n"
"2. 物料配置问题(如'物料样品板分配失败'\n"
"3. Bioyond系统状态异常\n"
f"请检查batch_create任务的执行结果"
)
if not order_ids: if not order_ids:
raise BioyondException("batch_create_result中未找到order_ids字段或为空") raise BioyondException("batch_create_result中未找到order_ids字段或为空")
@@ -1582,8 +1114,6 @@ class BioyondDispensingStation(BioyondWorkstation):
self.hardware_interface._logger.info( self.hardware_interface._logger.info(
f"成功获取任务 {order_code} 的实验报告" f"成功获取任务 {order_code} 的实验报告"
) )
# 简化报告,去除冗余信息
report = self._simplify_report(report)
reports.append({ reports.append({
"order_code": order_code, "order_code": order_code,
@@ -1758,7 +1288,7 @@ class BioyondDispensingStation(BioyondWorkstation):
f"开始执行批量物料转移: {len(transfer_groups)}组任务 -> {target_device_id}" f"开始执行批量物料转移: {len(transfer_groups)}组任务 -> {target_device_id}"
) )
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) from .config import WAREHOUSE_MAPPING
results = [] results = []
successful_count = 0 successful_count = 0
failed_count = 0 failed_count = 0

View File

@@ -6,7 +6,6 @@ Bioyond Workstation Implementation
""" """
import time import time
import traceback import traceback
import threading
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional, Union from typing import Dict, Any, List, Optional, Union
import json import json
@@ -24,94 +23,12 @@ from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
from pylabrobot.resources.resource import Resource as ResourcePLR from pylabrobot.resources.resource import Resource as ResourcePLR
from unilabos.devices.workstation.bioyond_studio.config import (
API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, HTTP_SERVICE_CONFIG
)
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
class ConnectionMonitor:
"""Bioyond连接监控器"""
def __init__(self, workstation, check_interval=30):
self.workstation = workstation
self.check_interval = check_interval
self._running = False
self._thread = None
self._last_status = "unknown"
def start(self):
if self._running:
return
self._running = True
self._thread = threading.Thread(target=self._monitor_loop, daemon=True, name="BioyondConnectionMonitor")
self._thread.start()
logger.info("Bioyond连接监控器已启动")
def stop(self):
self._running = False
if self._thread:
self._thread.join(timeout=2)
logger.info("Bioyond连接监控器已停止")
def _monitor_loop(self):
while self._running:
try:
# 使用 lightweight API 检查连接
# query_matial_type_list 是比较快的查询
start_time = time.time()
result = self.workstation.hardware_interface.material_type_list()
status = "online" if result else "offline"
msg = "Connection established" if status == "online" else "Failed to get material type list"
if status != self._last_status:
logger.info(f"Bioyond连接状态变更: {self._last_status} -> {status}")
self._publish_event(status, msg)
self._last_status = status
# 发布心跳 (可选,或者只在状态变更时发布)
# self._publish_event(status, msg)
except Exception as e:
logger.error(f"Bioyond连接检查异常: {e}")
if self._last_status != "error":
self._publish_event("error", str(e))
self._last_status = "error"
time.sleep(self.check_interval)
def _publish_event(self, status, message):
try:
if hasattr(self.workstation, "_ros_node") and self.workstation._ros_node:
event_data = {
"status": status,
"message": message,
"timestamp": datetime.now().isoformat()
}
# 动态发布消息,需要在 ROS2DeviceNode 中有对应支持
# 这里假设通用事件发布机制,使用 String 类型的 topic
# 话题: /<namespace>/events/device_status
ns = self.workstation._ros_node.namespace
topic = f"{ns}/events/device_status"
# 使用 ROS2DeviceNode 的发布功能
# 如果没有预定义的 publisher需要动态创建
# 注意workstation base node 可能没有自动创建 arbitrary publishers 的机制
# 这里我们先尝试用 String json 发布
# 在 ROS2DeviceNode 中通常需要先 create_publisher
# 为了简单起见,我们检查是否已有 publisher没有则创建
if not hasattr(self.workstation, "_device_status_pub"):
self.workstation._device_status_pub = self.workstation._ros_node.create_publisher(
String, topic, 10
)
self.workstation._device_status_pub.publish(
convert_to_ros_msg(String, json.dumps(event_data, ensure_ascii=False))
)
except Exception as e:
logger.error(f"发布设备状态事件失败: {e}")
class BioyondResourceSynchronizer(ResourceSynchronizer): class BioyondResourceSynchronizer(ResourceSynchronizer):
"""Bioyond资源同步器 """Bioyond资源同步器
@@ -257,8 +174,9 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
else: else:
logger.info(f"[同步→Bioyond] 物料不存在于 Bioyond将创建新物料并入库") logger.info(f"[同步→Bioyond] 物料不存在于 Bioyond将创建新物料并入库")
# 第1步从配置中获取仓库配置 # 第1步获取仓库配置
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) from .config import WAREHOUSE_MAPPING
warehouse_mapping = WAREHOUSE_MAPPING
# 确定目标仓库名称 # 确定目标仓库名称
parent_name = None parent_name = None
@@ -320,20 +238,14 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
# 第2步转换为 Bioyond 格式 # 第2步转换为 Bioyond 格式
logger.info(f"[同步→Bioyond] 🔄 转换物料为 Bioyond 格式...") logger.info(f"[同步→Bioyond] 🔄 转换物料为 Bioyond 格式...")
# 从配置中获取物料默认参数 # 导入物料默认参数配置
material_default_params = self.workstation.bioyond_config.get("material_default_parameters", {}) from .config import MATERIAL_DEFAULT_PARAMETERS
material_type_params = self.workstation.bioyond_config.get("material_type_parameters", {})
# 合并参数配置:物料名称参数 + typeId参数转换为 type:<uuid> 格式)
merged_params = material_default_params.copy()
for type_id, params in material_type_params.items():
merged_params[f"type:{type_id}"] = params
bioyond_material = resource_plr_to_bioyond( bioyond_material = resource_plr_to_bioyond(
[resource], [resource],
type_mapping=self.workstation.bioyond_config["material_type_mappings"], type_mapping=self.workstation.bioyond_config["material_type_mappings"],
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"], warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
material_params=merged_params material_params=MATERIAL_DEFAULT_PARAMETERS
)[0] )[0]
logger.info(f"[同步→Bioyond] 🔧 准备覆盖locations字段目标仓库: {parent_name}, 库位: {update_site}, UUID: {target_location_uuid[:8]}...") logger.info(f"[同步→Bioyond] 🔧 准备覆盖locations字段目标仓库: {parent_name}, 库位: {update_site}, UUID: {target_location_uuid[:8]}...")
@@ -556,20 +468,13 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
return material_bioyond_id return material_bioyond_id
# 转换为 Bioyond 格式 # 转换为 Bioyond 格式
# 从配置中获取物料默认参数 from .config import MATERIAL_DEFAULT_PARAMETERS
material_default_params = self.workstation.bioyond_config.get("material_default_parameters", {})
material_type_params = self.workstation.bioyond_config.get("material_type_parameters", {})
# 合并参数配置:物料名称参数 + typeId参数转换为 type:<uuid> 格式)
merged_params = material_default_params.copy()
for type_id, params in material_type_params.items():
merged_params[f"type:{type_id}"] = params
bioyond_material = resource_plr_to_bioyond( bioyond_material = resource_plr_to_bioyond(
[resource], [resource],
type_mapping=self.workstation.bioyond_config["material_type_mappings"], type_mapping=self.workstation.bioyond_config["material_type_mappings"],
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"], warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
material_params=merged_params material_params=MATERIAL_DEFAULT_PARAMETERS
)[0] )[0]
# ⚠️ 关键:创建物料时不设置 locations让 Bioyond 系统暂不分配库位 # ⚠️ 关键:创建物料时不设置 locations让 Bioyond 系统暂不分配库位
@@ -623,7 +528,8 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
logger.info(f"[物料入库] 目标库位: {update_site}") logger.info(f"[物料入库] 目标库位: {update_site}")
# 获取仓库配置和目标库位 UUID # 获取仓库配置和目标库位 UUID
warehouse_mapping = self.workstation.bioyond_config.get("warehouse_mapping", {}) from .config import WAREHOUSE_MAPPING
warehouse_mapping = WAREHOUSE_MAPPING
parent_name = None parent_name = None
target_location_uuid = None target_location_uuid = None
@@ -678,44 +584,6 @@ class BioyondWorkstation(WorkstationBase):
集成Bioyond物料管理的工作站实现 集成Bioyond物料管理的工作站实现
""" """
def _publish_task_status(
self,
task_id: str,
task_type: str,
status: str,
result: dict = None,
progress: float = 0.0,
task_code: str = None
):
"""发布任务状态事件"""
try:
if not getattr(self, "_ros_node", None):
return
event_data = {
"task_id": task_id,
"task_code": task_code,
"task_type": task_type,
"status": status,
"progress": progress,
"timestamp": datetime.now().isoformat()
}
if result:
event_data["result"] = result
topic = f"{self._ros_node.namespace}/events/task_status"
if not hasattr(self, "_task_status_pub"):
self._task_status_pub = self._ros_node.create_publisher(
String, topic, 10
)
self._task_status_pub.publish(
convert_to_ros_msg(String, json.dumps(event_data, ensure_ascii=False))
)
except Exception as e:
logger.error(f"发布任务状态事件失败: {e}")
def __init__( def __init__(
self, self,
bioyond_config: Optional[Dict[str, Any]] = None, bioyond_config: Optional[Dict[str, Any]] = None,
@@ -737,28 +605,10 @@ class BioyondWorkstation(WorkstationBase):
raise ValueError("Deck 配置不能为空,请在配置文件中添加正确的 deck 配置") raise ValueError("Deck 配置不能为空,请在配置文件中添加正确的 deck 配置")
# 初始化 warehouses 属性 # 初始化 warehouses 属性
if not hasattr(self.deck, "warehouses") or self.deck.warehouses is None: self.deck.warehouses = {}
self.deck.warehouses = {} for resource in self.deck.children:
if isinstance(resource, WareHouse):
# 仅当 warehouses 为空时尝试重新扫描(避免覆盖子类的修复) self.deck.warehouses[resource.name] = resource
if not self.deck.warehouses:
for resource in self.deck.children:
# 兼容性增强: 只要是仓库类别或者是 WareHouse 实例均可
is_warehouse = isinstance(resource, WareHouse) or getattr(resource, "category", "") == "warehouse"
# 如果配置中有定义,也可以认定为 warehouse
if not is_warehouse and "warehouse_mapping" in bioyond_config:
if resource.name in bioyond_config["warehouse_mapping"]:
is_warehouse = True
if is_warehouse:
self.deck.warehouses[resource.name] = resource
# 确保 category 被正确设置,方便后续使用
if getattr(resource, "category", "") != "warehouse":
try:
resource.category = "warehouse"
except:
pass
# 创建通信模块 # 创建通信模块
self._create_communication_module(bioyond_config) self._create_communication_module(bioyond_config)
@@ -777,22 +627,18 @@ class BioyondWorkstation(WorkstationBase):
self._set_workflow_mappings(bioyond_config["workflow_mappings"]) self._set_workflow_mappings(bioyond_config["workflow_mappings"])
# 准备 HTTP 报送接收服务配置(延迟到 post_init 启动) # 准备 HTTP 报送接收服务配置(延迟到 post_init 启动)
# 从 bioyond_config 中的 http_service_config 获取 # 从 bioyond_config 中获取,如果没有则使用 HTTP_SERVICE_CONFIG 的默认值
http_service_cfg = bioyond_config.get("http_service_config", {})
self._http_service_config = { self._http_service_config = {
"host": http_service_cfg.get("http_service_host", "127.0.0.1"), "host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG["http_service_host"]),
"port": http_service_cfg.get("http_service_port", 8080) "port": bioyond_config.get("http_service_port", HTTP_SERVICE_CONFIG["http_service_port"])
} }
self.http_service = None # 将在 post_init 启动 self.http_service = None # 将在 post_init 启动
self.connection_monitor = None # 将在 post_init 启动
logger.info(f"Bioyond工作站初始化完成") logger.info(f"Bioyond工作站初始化完成")
def __del__(self): def __del__(self):
"""析构函数:清理资源,停止 HTTP 服务""" """析构函数:清理资源,停止 HTTP 服务"""
try: try:
if hasattr(self, 'connection_monitor') and self.connection_monitor:
self.connection_monitor.stop()
if hasattr(self, 'http_service') and self.http_service is not None: if hasattr(self, 'http_service') and self.http_service is not None:
logger.info("正在停止 HTTP 报送服务...") logger.info("正在停止 HTTP 报送服务...")
self.http_service.stop() self.http_service.stop()
@@ -802,19 +648,8 @@ class BioyondWorkstation(WorkstationBase):
def post_init(self, ros_node: ROS2WorkstationNode): def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node self._ros_node = ros_node
# 启动连接监控
try:
self.connection_monitor = ConnectionMonitor(self)
self.connection_monitor.start()
except Exception as e:
logger.error(f"启动连接监控失败: {e}")
# 启动 HTTP 报送接收服务(现在 device_id 已可用) # 启动 HTTP 报送接收服务(现在 device_id 已可用)
# ⚠️ 检查子类是否已经自己管理 HTTP 服务 if hasattr(self, '_http_service_config'):
if self.bioyond_config.get("_disable_auto_http_service"):
logger.info("🔧 检测到 _disable_auto_http_service 标志,跳过自动启动 HTTP 服务")
logger.info(" 子类BioyondCellWorkstation已自行管理 HTTP 服务")
elif hasattr(self, '_http_service_config'):
try: try:
self.http_service = WorkstationHTTPService( self.http_service = WorkstationHTTPService(
workstation_instance=self, workstation_instance=self,
@@ -853,14 +688,19 @@ class BioyondWorkstation(WorkstationBase):
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
"""创建Bioyond通信模块""" """创建Bioyond通信模块"""
# 直接使用传入的配置,不再使用默认值 # 创建默认配置
# 所有配置必须从 JSON 文件中提供 default_config = {
**API_CONFIG,
"workflow_mappings": WORKFLOW_MAPPINGS,
"material_type_mappings": MATERIAL_TYPE_MAPPINGS,
"warehouse_mapping": WAREHOUSE_MAPPING
}
# 如果传入了 config合并配置config 中的值会覆盖默认值)
if config: if config:
self.bioyond_config = config self.bioyond_config = {**default_config, **config}
else: else:
# 如果没有配置,使用空字典(会导致后续错误,但这是预期的) self.bioyond_config = default_config
self.bioyond_config = {}
print("警告: 未提供 bioyond_config请确保在 JSON 配置文件中提供完整配置")
self.hardware_interface = BioyondV1RPC(self.bioyond_config) self.hardware_interface = BioyondV1RPC(self.bioyond_config)
@@ -1174,15 +1014,7 @@ class BioyondWorkstation(WorkstationBase):
workflow_id = self._get_workflow(actual_workflow_name) workflow_id = self._get_workflow(actual_workflow_name)
if workflow_id: if workflow_id:
# 兼容 BioyondReactionStation 中 workflow_sequence 被重写为 property 的情况 self.workflow_sequence.append(workflow_id)
if isinstance(self.workflow_sequence, list):
self.workflow_sequence.append(workflow_id)
elif hasattr(self, "_cached_workflow_sequence") and isinstance(self._cached_workflow_sequence, list):
self._cached_workflow_sequence.append(workflow_id)
else:
print(f"❌ 无法添加工作流: workflow_sequence 类型错误 {type(self.workflow_sequence)}")
return False
print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}") print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}")
return True return True
return False return False
@@ -1383,22 +1215,6 @@ class BioyondWorkstation(WorkstationBase):
# TODO: 根据实际业务需求处理步骤完成逻辑 # TODO: 根据实际业务需求处理步骤完成逻辑
# 例如:更新数据库、触发后续流程等 # 例如:更新数据库、触发后续流程等
# 发布任务状态事件 (running/progress update)
self._publish_task_status(
task_id=data.get('orderCode'), # 使用 OrderCode 作为关联 ID
task_code=data.get('orderCode'),
task_type="bioyond_step",
status="running",
progress=0.5, # 步骤完成视为任务进行中
result={"step_name": data.get('stepName'), "step_id": data.get('stepId')}
)
# 更新物料信息
# 步骤完成后,物料状态可能发生变化(如位置、用量等),触发同步
logger.info(f"[步骤完成报送] 触发物料同步...")
self.resource_synchronizer.sync_from_external()
return { return {
"processed": True, "processed": True,
"step_id": data.get('stepId'), "step_id": data.get('stepId'),
@@ -1433,17 +1249,6 @@ class BioyondWorkstation(WorkstationBase):
# TODO: 根据实际业务需求处理通量完成逻辑 # TODO: 根据实际业务需求处理通量完成逻辑
# 发布任务状态事件
self._publish_task_status(
task_id=data.get('orderCode'),
task_code=data.get('orderCode'),
task_type="bioyond_sample",
status="running",
progress=0.7,
result={"sample_id": data.get('sampleId'), "status": status_desc}
)
return { return {
"processed": True, "processed": True,
"sample_id": data.get('sampleId'), "sample_id": data.get('sampleId'),
@@ -1483,32 +1288,6 @@ class BioyondWorkstation(WorkstationBase):
# TODO: 根据实际业务需求处理任务完成逻辑 # TODO: 根据实际业务需求处理任务完成逻辑
# 例如:更新物料库存、生成报表等 # 例如:更新物料库存、生成报表等
# 映射状态到事件状态
event_status = "completed"
if str(data.get('status')) in ["-11", "-12"]:
event_status = "error"
elif str(data.get('status')) == "30":
event_status = "completed"
else:
event_status = "running" # 其他状态视为运行中(或根据实际定义)
# 发布任务状态事件
self._publish_task_status(
task_id=data.get('orderCode'),
task_code=data.get('orderCode'),
task_type="bioyond_order",
status=event_status,
progress=1.0 if event_status in ["completed", "error"] else 0.9,
result={"order_name": data.get('orderName'), "status": status_desc, "materials_count": len(used_materials)}
)
# 更新物料信息
# 任务完成后,且状态为完成时,触发同步以更新最终物料状态
if event_status == "completed":
logger.info(f"[任务完成报送] 触发物料同步...")
self.resource_synchronizer.sync_from_external()
return { return {
"processed": True, "processed": True,
"order_code": data.get('orderCode'), "order_code": data.get('orderCode'),

View File

@@ -1,84 +0,0 @@
# Modbus CSV 地址映射说明
本文档说明 `coin_cell_assembly_a.csv` 文件如何将命名节点映射到实际的 Modbus 地址,以及如何在代码中使用它们。
## 1. CSV 文件结构
地址表文件位于同级目录下:`coin_cell_assembly_a.csv`
每一行定义了一个 Modbus 节点,包含以下关键列:
| 列名 | 说明 | 示例 |
|------|------|------|
| **Name** | **节点名称** (代码中引用的 Key) | `COIL_ALUMINUM_FOIL` |
| **DataType** | 数据类型 (BOOL, INT16, FLOAT32, STRING) | `BOOL` |
| **Comment** | 注释说明 | `使用铝箔垫` |
| **Attribute** | 属性 (通常留空或用于额外标记) | |
| **DeviceType** | Modbus 寄存器类型 (`coil`, `hold_register`) | `coil` |
| **Address** | **Modbus 地址** (十进制) | `8340` |
### 示例行 (铝箔垫片)
```csv
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340,
```
- **名称**: `COIL_ALUMINUM_FOIL`
- **类型**: `coil` (线圈,读写单个位)
- **地址**: `8340`
---
## 2. 加载与注册流程
`coin_cell_assembly.py` 的初始化代码中:
1. **加载 CSV**: `BaseClient.load_csv()` 读取 CSV 并解析每行定义。
2. **注册节点**: `modbus_client.register_node_list()` 将解析后的节点注册到 Modbus 客户端实例中。
```python
# 代码位置: coin_cell_assembly.py (L174-175)
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv'))
self.client = modbus_client.register_node_list(self.nodes)
```
---
## 3. 代码中的使用方式
注册后,通过 `self.client.use_node('节点名称')` 即可获取该节点对象并进行读写操作,无需关心具体地址。
### 控制铝箔垫片 (COIL_ALUMINUM_FOIL)
```python
# 代码位置: qiming_coin_cell_code 函数 (L1048)
self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian)
```
- **写入 True**: 对应 Modbus 功能码 05 (Write Single Coil),向地址 `8340` 写入 `1` (ON)。
- **写入 False**: 向地址 `8340` 写入 `0` (OFF)。
> **注意**: 代码中使用了 `not lvbodian`,这意味着逻辑是反转的。如果 `lvbodian` 参数为 `True` (默认),写入的是 `False` (不使用铝箔垫)。
---
## 4. 地址转换注意事项 (Modbus vs PLC)
CSV 中的 `Address` 列(如 `8340`)是 **Modbus 协议地址**
如果使用 InoProShop (汇川 PLC 编程软件),看到的可能是 **PLC 内部地址** (如 `%QX...``%MW...`)。这两者之间通常需要转换。
### 常见的转换规则 (示例)
- **Coil (线圈) %QX**:
- `Modbus地址 = 字节地址 * 8 + 位偏移`
- *例子*: `%QX834.0` -> `834 * 8 + 0` = `6672`
- *注意*: 如果 CSV 中配置的是 `8340`,这可能是一个自定义映射,或者是基于不同规则(如直接对应 Word 地址的某种映射,或者可能就是地址写错了/使用了非标准映射)。
- **Register (寄存器) %MW**:
- 通常直接对应,或者有偏移量 (如 Modbus 40001 = PLC MW0)。
### 验证方法
由于 `test_unilab_interact.py` 中发现 `8450` (CSV风格) 不工作,而 `6760` (%QX845.0 计算值) 工作正常,**建议对 CSV 中的其他地址也进行核实**,特别是像 `8340` 这样以 0 结尾看起来像是 "字节地址+0" 的数值,可能实际上应该是 `%QX834.0` 对应的 `6672`
如果发现设备控制无反应,请尝试按照标准的 Modbus 计算方式转换 PLC 地址。

View File

@@ -1,352 +0,0 @@
# 2026-01-13 物料搜寻确认弹窗自动处理功能
## 概述
本次更新为设备初始化流程添加了**物料搜寻确认弹窗自动检测与处理功能**。在设备初始化过程中PLC 会弹出物料搜寻确认对话框,现在系统可以根据用户参数自动点击"是"或"否"按钮,无需手动干预。
## 背景问题
### 原有流程
1. 调用 `func_pack_device_init_auto_start_combined()` 初始化设备
2. PLC 在初始化过程中弹出物料搜寻确认对话框
3. **需要人工手动点击**"是"或"否"按钮
4. PLC 继续完成初始化并启动
### 存在的问题
- 需要人工干预,无法实现全自动化
- 影响批量生产效率
- 容易遗忘点击导致流程卡住
## 解决方案
### 新增 Modbus 地址配置
`coin_cell_assembly_b.csv` 第 69-71 行添加三个 coil
| Name | DeviceType | Address | 说明 |
|------|-----------|---------|------|
| COIL_MATERIAL_SEARCH_DIALOG_APPEAR | coil | 6470 | 物料搜寻确认弹窗画面是否出现 |
| COIL_MATERIAL_SEARCH_CONFIRM_YES | coil | 6480 | 初始化物料搜寻确认按钮"是" |
| COIL_MATERIAL_SEARCH_CONFIRM_NO | coil | 6490 | 初始化物料搜寻确认按钮"否" |
**Modbus 地址转换:**
- CSV 6470 → Modbus 5176 (弹窗出现)
- CSV 6480 → Modbus 5184 (按钮"是")
- CSV 6490 → Modbus 5192 (按钮"否")
## 代码修改详情
### 1. coin_cell_assembly.py
#### 1.1 新增辅助方法 `_handle_material_search_dialog()`
**位置:** 第 799-901 行
**功能:**
- 监测物料搜寻确认弹窗是否出现Coil 5176
- 根据 `enable_search` 参数自动点击对应按钮
- 使用**脉冲模式**模拟真实按钮操作:`True` → 保持 0.5 秒 → `False`
**参数:**
- `enable_search: bool` - True=点击"是"(启用物料搜寻), False=点击"否"(不启用)
- `timeout: int = 30` - 等待弹窗出现的最大时间(秒)
**逻辑流程:**
```python
1. 监测 COIL_MATERIAL_SEARCH_DIALOG_APPEAR ( 0.5 秒检查一次)
2. 检测到弹窗出现 (Coil = True)
3. 选择按钮
- enable_search=True COIL_MATERIAL_SEARCH_CONFIRM_YES
- enable_search=False COIL_MATERIAL_SEARCH_CONFIRM_NO
4. 执行脉冲操作
- 写入 True (按下按钮)
- 等待 0.5
- 写入 False (释放按钮)
- 验证状态
```
#### 1.2 修改 `func_pack_device_init_auto_start_combined()`
**位置:** 第 904-1115 行
**主要改动:**
1. **添加新参数**
```python
def func_pack_device_init_auto_start_combined(
self,
material_search_enable: bool = False # 新增参数
) -> bool:
```
2. **内联初始化逻辑并集成弹窗检测**
- 不再调用 `self.func_pack_device_init()`
- 将初始化逻辑直接实现在函数内
- **在等待初始化完成的循环中实时检测弹窗**
- 避免死锁PLC 等待弹窗确认 ↔ 代码等待初始化完成
3. **关键代码片段**
```python
# 等待初始化完成,同时检测物料搜寻弹窗
while (self._sys_init_status()) == False:
# 检查超时
if time.time() - start_wait > max_wait_time:
raise RuntimeError(f"初始化超时")
# 如果还没处理弹窗,检测弹窗是否出现
if not dialog_handled:
dialog_state = self.client.use_node('COIL_MATERIAL_SEARCH_DIALOG_APPEAR').read(1)
if dialog_actual: # 弹窗出现
# 执行脉冲按钮点击
button_node.write(True) # 按下
time.sleep(0.5) # 保持
button_node.write(False) # 释放
dialog_handled = True
time.sleep(1)
```
4. **步骤调整**
- 步骤 0: 前置条件检查
- 步骤 1: 设备初始化(**包含弹窗检测**
- 步骤 1.5: 已在步骤 1 中完成
- 步骤 2: 切换自动模式
- 步骤 3: 启动设备
### 2. coin_cell_workstation.yaml
**位置:** 第 292-312 行
**修改内容:**
```yaml
auto-func_pack_device_init_auto_start_combined:
goal_default:
material_search_enable: false # 新增默认值
schema:
description: 组合函数:设备初始化 + 物料搜寻确认 + 切换自动模式 + 启动。初始化过程中会自动检测物料搜寻确认弹窗,并根据参数自动点击"是"或"否"按钮
goal:
properties:
material_search_enable: # 新增参数配置
default: false
description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻)
type: boolean
```
### 3. 测试脚本(已创建,用户已删除)
#### 3.1 test_material_search_dialog.py
- 从 CSV 动态加载 Modbus 地址
- 支持 4 种测试模式:
- `query` - 查询所有状态
- `dialog <0|1>` - 设置弹窗出现/消失
- `yes` - 脉冲点击"是"按钮
- `no` - 脉冲点击"否"按钮
- 兼容 pymodbus 3.x API
#### 3.2 更新其他测试脚本
- `test_coin_cell_reset.py` - 更新为 pymodbus 3.x API
- `test_unilab_interact.py` - 更新为 pymodbus 3.x API
## 使用方法
### 参数说明
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `material_search_enable` | boolean | `false` | 是否启用物料搜寻功能 |
### 调用示例
#### 1. 不启用物料搜寻(默认)
```python
# 默认参数,点击"否"按钮
await device.func_pack_device_init_auto_start_combined()
```
或在 YAML workflow 中:
```yaml
# 使用默认值 false不启用物料搜寻
- BatteryStation/auto-func_pack_device_init_auto_start_combined: {}
```
#### 2. 启用物料搜寻
```python
# 显式设置为 True点击"是"按钮
await device.func_pack_device_init_auto_start_combined(
material_search_enable=True
)
```
或在 YAML workflow 中:
```yaml
- BatteryStation/auto-func_pack_device_init_auto_start_combined:
goal:
material_search_enable: true # 启用物料搜寻
```
## 执行日志示例
```
26-01-13 [21:32:44] [INFO] 开始组合操作:设备初始化 → 物料搜寻确认 → 自动模式 → 启动
26-01-13 [21:32:44] [INFO] 【步骤 0/4】前置条件检查...
26-01-13 [21:32:44] [INFO] ✓ REG_UNILAB_INTERACT 检查通过
26-01-13 [21:32:44] [INFO] ✓ COIL_GB_L_IGNORE_CMD 检查通过
26-01-13 [21:32:44] [INFO] 【步骤 1/4】设备初始化...
26-01-13 [21:32:44] [INFO] 切换手动模式...
26-01-13 [21:32:46] [INFO] 发送初始化命令...
26-01-13 [21:32:47] [INFO] 等待初始化完成(同时监测物料搜寻弹窗)...
26-01-13 [21:33:05] [INFO] ✓ 在初始化过程中检测到物料搜寻确认弹窗!
26-01-13 [21:33:05] [INFO] 用户选择: 不启用物料搜寻(点击否)
26-01-13 [21:33:05] [INFO] → 按下按钮 '否'
26-01-13 [21:33:06] [INFO] → 释放按钮 '否'
26-01-13 [21:33:07] [INFO] ✓ 成功处理物料搜寻确认弹窗(选择: 否)
26-01-13 [21:33:08] [INFO] ✓ 初始化状态完成
26-01-13 [21:33:12] [INFO] ✓ 设备初始化完成
26-01-13 [21:33:12] [INFO] 【步骤 1.5/4】物料搜寻确认已在初始化过程中完成
26-01-13 [21:33:12] [INFO] 【步骤 2/4】切换自动模式...
26-01-13 [21:33:15] [INFO] ✓ 切换自动模式完成
26-01-13 [21:33:15] [INFO] 【步骤 3/4】启动设备...
26-01-13 [21:33:18] [INFO] ✓ 启动设备完成
26-01-13 [21:33:18] [INFO] 组合操作完成:设备已成功初始化、确认物料搜寻、切换自动模式并启动
```
## 技术要点
### 1. 脉冲模式按钮操作
模拟真实按钮按压过程:
1. 写入 `True` (按下)
2. 保持 0.5 秒
3. 写入 `False` (释放)
4. 验证状态
### 2. 避免死锁
**问题:** PLC 在初始化过程中等待弹窗确认,而代码等待初始化完成
**解决:** 在初始化等待循环中实时检测弹窗,一旦出现立即处理
### 3. 超时保护
- 弹窗检测超时30 秒(在 `_handle_material_search_dialog` 中)
- 初始化超时120 秒(在 `func_pack_device_init_auto_start_combined` 中)
### 4. PyModbus 3.x API 兼容
所有 Modbus 操作使用 keyword arguments
```python
# 读取
client.read_coils(address=5176, count=1)
# 写入
client.write_coil(address=5184, value=True)
```
## 向后兼容性
### 保留的原有函数
- `func_pack_device_init()` - 单独的初始化函数,不包含弹窗处理
- 仍可在 YAML 中通过 `auto-func_pack_device_init` 调用
- 用于不需要自动处理弹窗的场景
### 新增的功能
- 在 `func_pack_device_init_auto_start_combined()` 中集成弹窗处理
- 通过参数控制,默认行为与之前兼容(点击"否"
## 验证测试
### 测试场景
#### 场景 1默认参数不启用物料搜寻
```bash
# 调用时不传参数
BatteryStation/auto-func_pack_device_init_auto_start_combined: {}
```
**预期结果:**
- ✅ 检测到弹窗
- ✅ 自动点击"否"按钮
- ✅ 初始化完成并启动成功
#### 场景 2启用物料搜寻
```bash
# 设置 material_search_enable=true
BatteryStation/auto-func_pack_device_init_auto_start_combined:
goal:
material_search_enable: true
```
**预期结果:**
- ✅ 检测到弹窗
- ✅ 自动点击"是"按钮
- ✅ 初始化完成并启动成功
### 实际测试结果
**测试时间:** 2026-01-13 21:32:43
**测试参数:** `material_search_enable: false`
**测试结果:** ✅ 成功
**关键时间节点:**
- 21:33:05 - 检测到弹窗
- 21:33:05 - 按下"否"按钮
- 21:33:06 - 释放"否"按钮
- 21:33:07 - 弹窗处理完成
- 21:33:08 - 初始化状态完成
- 21:33:18 - 整个流程完成
**总耗时:** 约 35 秒(包含初始化全过程)
## 注意事项
1. **CSV 配置依赖**
- 确保 `coin_cell_assembly_b.csv` 包含 69-71 行的 coil 配置
- 地址转换逻辑:`modbus_addr = (csv_addr // 10) * 8 + (csv_addr % 10)`
2. **默认行为**
- 默认 `material_search_enable=false`,即不启用物料搜寻
- 如需启用,必须显式设置为 `true`
3. **日志级别**
- 弹窗检测过程中的 `waiting for init_cmd` 使用 DEBUG 级别
- 关键操作(检测到弹窗、按钮操作)使用 INFO 级别
4. **原有函数保留**
- `func_pack_device_init()` 仍然可用,但不包含弹窗处理
- 如果单独调用此函数,仍需手动处理弹窗
## 文件清单
### 修改的文件
1. `d:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly\coin_cell_assembly.py`
- 新增 `_handle_material_search_dialog()` 方法
- 修改 `func_pack_device_init_auto_start_combined()` 函数
2. `d:\UniLabdev\Uni-Lab-OS\unilabos\registry\devices\coin_cell_workstation.yaml`
- 更新 `auto-func_pack_device_init_auto_start_combined` 配置
- 添加 `material_search_enable` 参数说明
3. `d:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly\coin_cell_assembly_b.csv`
- 第 69-71 行添加三个 coil 配置
### 创建的测试文件(已删除)
1. `test_material_search_dialog.py` - 物料搜寻弹窗测试脚本
2. `test_coin_cell_reset.py` - 复位功能测试(更新为 pymodbus 3.x
3. `test_unilab_interact.py` - Unilab 交互测试(更新为 pymodbus 3.x
## 总结
本次更新成功实现了设备初始化过程中物料搜寻确认弹窗的自动化处理,主要优势:
**全自动化** - 无需人工干预
**参数可配** - 灵活控制是否启用物料搜寻
**实时检测** - 在初始化等待循环中检测,避免死锁
**脉冲模式** - 模拟真实按钮操作
**向后兼容** - 保留原有函数,不影响现有流程
**完整日志** - 详细记录每一步操作
**超时保护** - 防止无限等待
该功能已通过实际测试验证,可投入生产使用。
---
**文档版本:** 1.0
**创建日期:** 2026-01-13
**作者:** Antigravity AI Assistant
**最后更新:** 2026-01-13 21:36

View File

@@ -1,645 +0,0 @@
"""
纽扣电池组装工作站物料类定义
Button Battery Assembly Station Resource Classes
"""
from __future__ import annotations
from collections import OrderedDict
from typing import Any, Dict, List, Optional, TypedDict, Union, cast
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.container import Container
from pylabrobot.resources.deck import Deck
from pylabrobot.resources.itemized_resource import ItemizedResource
from pylabrobot.resources.resource import Resource
from pylabrobot.resources.resource_stack import ResourceStack
from pylabrobot.resources.tip_rack import TipRack, TipSpot
from pylabrobot.resources.trash import Trash
from pylabrobot.resources.utils import create_ordered_items_2d
from unilabos.resources.battery.magazine import MagazineHolder_4_Cathode, MagazineHolder_6_Cathode, MagazineHolder_6_Anode, MagazineHolder_6_Battery
from unilabos.resources.battery.bottle_carriers import YIHUA_Electrolyte_12VialCarrier
from unilabos.resources.battery.electrode_sheet import ElectrodeSheet
# TODO: 这个应该只能放一个极片
class MaterialHoleState(TypedDict):
diameter: int
depth: int
max_sheets: int
info: Optional[str] # 附加信息
class MaterialHole(Resource):
"""料板洞位类"""
children: List[ElectrodeSheet] = []
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str = "material_hole",
**kwargs
):
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
)
self._unilabos_state: MaterialHoleState = MaterialHoleState(
diameter=20,
depth=10,
max_sheets=1,
info=None
)
def get_all_sheet_info(self):
info_list = []
for sheet in self.children:
info_list.append(sheet._unilabos_state["info"])
return info_list
#这个函数函数好像没用,一般不会集中赋值质量
def set_all_sheet_mass(self):
for sheet in self.children:
sheet._unilabos_state["mass"] = 0.5 # 示例设置质量为0.5g
def load_state(self, state: Dict[str, Any]) -> None:
"""格式不变"""
super().load_state(state)
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
"""格式不变"""
data = super().serialize_state()
data.update(self._unilabos_state) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
#移动极片前先取出对象
def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]:
for sheet in self.children:
if sheet.name == name:
return sheet
return None
def has_electrode_sheet(self) -> bool:
"""检查洞位是否有极片"""
return len(self.children) > 0
def assign_child_resource(
self,
resource: ElectrodeSheet,
location: Optional[Coordinate],
reassign: bool = True,
):
"""放置极片"""
# TODO: 这里要改diameter找不到加入._unilabos_state后应该没问题
#if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]:
# raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}")
#if len(self.children) >= self._unilabos_state["max_sheets"]:
# raise ValueError(f"洞位已满,无法放置更多极片")
super().assign_child_resource(resource, location, reassign)
# 根据children的编号取物料对象。
def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet:
return self.children[index]
class MaterialPlateState(TypedDict):
hole_spacing_x: float
hole_spacing_y: float
hole_diameter: float
info: Optional[str] # 附加信息
class MaterialPlate(ItemizedResource[MaterialHole]):
"""料板类 - 4x4个洞位每个洞位放1个极片"""
children: List[MaterialHole]
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
ordered_items: Optional[Dict[str, MaterialHole]] = None,
ordering: Optional[OrderedDict[str, str]] = None,
category: str = "material_plate",
model: Optional[str] = None,
fill: bool = False
):
"""初始化料板
Args:
name: 料板名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing_x: X方向洞位间距 (mm)
hole_spacing_y: Y方向洞位间距 (mm)
number: 编号
category: 类别
model: 型号
"""
self._unilabos_state: MaterialPlateState = MaterialPlateState(
hole_spacing_x=24.0,
hole_spacing_y=24.0,
hole_diameter=20.0,
info="",
)
# 创建4x4的洞位
# TODO: 这里要改,对应不同形状
holes = create_ordered_items_2d(
klass=MaterialHole,
num_items_x=4,
num_items_y=4,
dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
dz=size_z,
item_dx=self._unilabos_state["hole_spacing_x"],
item_dy=self._unilabos_state["hole_spacing_y"],
size_x = 16,
size_y = 16,
size_z = 16,
)
if fill:
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
else:
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=ordered_items,
ordering=ordering,
category=category,
model=model,
)
def update_locations(self):
# TODO:调多次相加
holes = create_ordered_items_2d(
klass=MaterialHole,
num_items_x=4,
num_items_y=4,
dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
dz=self._size_z,
item_dx=self._unilabos_state["hole_spacing_x"],
item_dy=self._unilabos_state["hole_spacing_y"],
size_x = 1,
size_y = 1,
size_z = 1,
)
for item, original_item in zip(holes.items(), self.children):
original_item.location = item[1].location
class PlateSlot(ResourceStack):
"""板槽位类 - 1个槽上能堆放8个板移板只能操作最上方的板"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
max_plates: int = 8,
category: str = "plate_slot",
model: Optional[str] = None
):
"""初始化板槽位
Args:
name: 槽位名称
max_plates: 最大板数量
category: 类别
"""
super().__init__(
name=name,
direction="z", # Z方向堆叠
resources=[],
)
self.max_plates = max_plates
self.category = category
def can_add_plate(self) -> bool:
"""检查是否可以添加板"""
return len(self.children) < self.max_plates
def add_plate(self, plate: MaterialPlate) -> None:
"""添加料板"""
if not self.can_add_plate():
raise ValueError(f"槽位 {self.name} 已满,无法添加更多板")
self.assign_child_resource(plate)
def get_top_plate(self) -> MaterialPlate:
"""获取最上方的板"""
if len(self.children) == 0:
raise ValueError(f"槽位 {self.name} 为空")
return cast(MaterialPlate, self.get_top_item())
def take_top_plate(self) -> MaterialPlate:
"""取出最上方的板"""
top_plate = self.get_top_plate()
self.unassign_child_resource(top_plate)
return top_plate
def can_access_for_picking(self) -> bool:
"""检查是否可以进行取料操作(只有最上方的板能进行取料操作)"""
return len(self.children) > 0
def serialize(self) -> dict:
return {
**super().serialize(),
"max_plates": self.max_plates,
}
#是一种类型注解不用self
class BatteryState(TypedDict):
"""电池状态字典"""
diameter: float
height: float
assembly_pressure: float
electrolyte_volume: float
electrolyte_name: str
class Battery(Resource):
"""电池类 - 可容纳极片"""
children: List[ElectrodeSheet] = []
def __init__(
self,
name: str,
size_x=1,
size_y=1,
size_z=1,
category: str = "battery",
):
"""初始化电池
Args:
name: 电池名称
diameter: 直径 (mm)
height: 高度 (mm)
max_volume: 最大容量 (μL)
barcode: 二维码编号
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=1,
size_y=1,
size_z=1,
category=category,
)
self._unilabos_state: BatteryState = BatteryState(
diameter = 1.0,
height = 1.0,
assembly_pressure = 1.0,
electrolyte_volume = 1.0,
electrolyte_name = "DP001"
)
def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool:
to_add_name = bottle._unilabos_state["electrolyte_name"]
if bottle.aspirate_electrolyte(10):
if self.add_electrolyte(to_add_name, 10):
pass
else:
bottle._unilabos_state["electrolyte_volume"] += 10
def set_electrolyte(self, name: str, volume: float) -> None:
"""设置电解液信息"""
self._unilabos_state["electrolyte_name"] = name
self._unilabos_state["electrolyte_volume"] = volume
#这个应该没用,不会有加了后再加的事情
def add_electrolyte(self, name: str, volume: float) -> bool:
"""添加电解液信息"""
if name != self._unilabos_state["electrolyte_name"]:
return False
self._unilabos_state["electrolyte_volume"] += volume
def load_state(self, state: Dict[str, Any]) -> None:
"""格式不变"""
super().load_state(state)
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
"""格式不变"""
data = super().serialize_state()
data.update(self._unilabos_state) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
# 电解液作为属性放进去
class BatteryPressSlotState(TypedDict):
"""电池状态字典"""
diameter: float =20.0
depth: float = 4.0
class BatteryPressSlot(Resource):
"""电池压制槽类 - 设备,可容纳一个电池"""
children: List[Battery] = []
def __init__(
self,
name: str = "BatteryPressSlot",
category: str = "battery_press_slot",
):
"""初始化电池压制槽
Args:
name: 压制槽名称
diameter: 直径 (mm)
depth: 深度 (mm)
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=10,
size_y=12,
size_z=13,
category=category,
)
self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState()
def has_battery(self) -> bool:
"""检查是否有电池"""
return len(self.children) > 0
def load_state(self, state: Dict[str, Any]) -> None:
"""格式不变"""
super().load_state(state)
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
"""格式不变"""
data = super().serialize_state()
data.update(self._unilabos_state) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
def assign_child_resource(
self,
resource: Battery,
location: Optional[Coordinate],
reassign: bool = True,
):
"""放置极片"""
# TODO: 让高京看下槽位只有一个电池时是否这么写。
if self.has_battery():
raise ValueError(f"槽位已含有一个电池,无法再放置其他电池")
super().assign_child_resource(resource, location, reassign)
# 根据children的编号取物料对象。
def get_battery_info(self, index: int) -> Battery:
return self.children[0]
def TipBox64(
name: str,
size_x: float = 127.8,
size_y: float = 85.5,
size_z: float = 60.0,
category: str = "tip_rack",
model: Optional[str] = None,
):
"""64孔枪头盒类"""
from pylabrobot.resources.tip import Tip
# 创建12x8=96个枪头位
def make_tip():
return Tip(
has_filter=False,
total_tip_length=20.0,
maximal_volume=1000, # 1mL
fitting_depth=8.0,
)
tip_spots = create_ordered_items_2d(
klass=TipSpot,
num_items_x=12,
num_items_y=8,
dx=8.0,
dy=8.0,
dz=0.0,
item_dx=9.0,
item_dy=9.0,
size_x=10,
size_y=10,
size_z=0.0,
make_tip=make_tip,
)
idx_available = list(range(0, 32)) + list(range(64, 96))
tip_spots_available = {k: v for i, (k, v) in enumerate(tip_spots.items()) if i in idx_available}
tip_rack = TipRack(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
# ordered_items=tip_spots_available,
ordered_items=tip_spots,
category=category,
model=model,
with_tips=False,
)
tip_rack.set_tip_state([True]*32 + [False]*32 + [True]*32) # 前32和后32个有枪头中间32个无枪头
return tip_rack
class WasteTipBoxstate(TypedDict):
""""废枪头盒状态字典"""
max_tips: int = 100
tip_count: int = 0
#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断
class WasteTipBox(Trash):
"""废枪头盒类 - 100个枪头容量"""
def __init__(
self,
name: str,
size_x: float = 127.8,
size_y: float = 85.5,
size_z: float = 60.0,
material_z_thickness=0,
max_volume=float("inf"),
category="trash",
model=None,
compute_volume_from_height=None,
compute_height_from_volume=None,
):
"""初始化废枪头盒
Args:
name: 废枪头盒名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
max_tips: 最大枪头容量
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
model=model,
)
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
def add_tip(self) -> None:
"""添加废枪头"""
if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]:
raise ValueError(f"废枪头盒 {self.name} 已满")
self._unilabos_state["tip_count"] += 1
def get_tip_count(self) -> int:
"""获取枪头数量"""
return self._unilabos_state["tip_count"]
def empty(self) -> None:
"""清空废枪头盒"""
self._unilabos_state["tip_count"] = 0
def load_state(self, state: Dict[str, Any]) -> None:
"""格式不变"""
super().load_state(state)
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
"""格式不变"""
data = super().serialize_state()
data.update(self._unilabos_state) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
class CoincellDeck(Deck):
"""纽扣电池组装工作站台面类"""
def __init__(
self,
name: str = "coin_cell_deck",
size_x: float = 1450.0, # 1m
size_y: float = 1450.0, # 1m
size_z: float = 100.0, # 0.9m
origin: Coordinate = Coordinate(-2200, 0, 0),
category: str = "coin_cell_deck",
setup: bool = False, # 是否自动执行 setup
):
"""初始化纽扣电池组装工作站台面
Args:
name: 台面名称
size_x: 长度 (mm) - 1m
size_y: 宽度 (mm) - 1m
size_z: 高度 (mm) - 0.9m
origin: 原点坐标
category: 类别
setup: 是否自动执行 setup 配置标准布局
"""
super().__init__(
name=name,
size_x=1450.0,
size_y=1450.0,
size_z=100.0,
origin=origin,
)
if setup:
self.setup()
def setup(self) -> None:
"""设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置"""
# ====================================== 子弹夹 ============================================
# 正极片4个洞位2x2布局
zhengji_zip = MagazineHolder_4_Cathode("正极&铝箔弹夹")
self.assign_child_resource(zhengji_zip, Coordinate(x=402.0, y=830.0, z=0))
# 正极壳、平垫片6个洞位2x2+2布局
zhengjike_zip = MagazineHolder_6_Cathode("正极壳&平垫片弹夹")
self.assign_child_resource(zhengjike_zip, Coordinate(x=566.0, y=272.0, z=0))
# 负极壳、弹垫片6个洞位2x2+2布局
fujike_zip = MagazineHolder_6_Anode("负极壳&弹垫片弹夹")
self.assign_child_resource(fujike_zip, Coordinate(x=474.0, y=276.0, z=0))
# 成品弹夹6个洞位3x2布局
chengpindanjia_zip = MagazineHolder_6_Battery("成品弹夹")
self.assign_child_resource(chengpindanjia_zip, Coordinate(x=260.0, y=156.0, z=0))
# ====================================== 物料板 ============================================
# 创建物料板料盘carrier- 4x4布局
# 负极料盘
fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(fujiliaopan, Coordinate(x=708.0, y=794.0, z=0))
# for i in range(16):
# fujipian = ElectrodeSheet(name=f"{fujiliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
# fujiliaopan.children[i].assign_child_resource(fujipian, location=None)
# 隔膜料盘
gemoliaopan = MaterialPlate(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(gemoliaopan, Coordinate(x=718.0, y=918.0, z=0))
# for i in range(16):
# gemopian = ElectrodeSheet(name=f"{gemoliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
# gemoliaopan.children[i].assign_child_resource(gemopian, location=None)
# ====================================== 瓶架、移液枪 ============================================
# 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒
# 奔耀上料5ml分液瓶小板 - 由奔曜跨站转运而来,不单独写,但是这里应该有一个堆栈用于摆放分液瓶小板
# bottle_rack_3x4 = BottleRack(
# name="bottle_rack_3x4",
# size_x=210.0,
# size_y=140.0,
# size_z=100.0,
# num_items_x=2,
# num_items_y=4,
# position_spacing=35.0,
# orientation="vertical",
# )
# self.assign_child_resource(bottle_rack_3x4, Coordinate(x=1542.0, y=717.0, z=0))
# 电解液缓存位 - 6x2布局
bottle_rack_6x2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2")
self.assign_child_resource(bottle_rack_6x2, Coordinate(x=1050.0, y=358.0, z=0))
# 电解液回收位6x2
bottle_rack_6x2_2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2_2")
self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=914.0, y=358.0, z=0))
tip_box = TipBox64(name="tip_box_64")
self.assign_child_resource(tip_box, Coordinate(x=782.0, y=514.0, z=0))
waste_tip_box = WasteTipBox(name="waste_tip_box")
self.assign_child_resource(waste_tip_box, Coordinate(x=778.0, y=622.0, z=0))
def YH_Deck(name=""):
cd = CoincellDeck(name=name)
cd.setup()
return cd
if __name__ == "__main__":
deck = create_coin_cell_deck()
print(deck)

File diff suppressed because it is too large Load Diff

View File

@@ -1,133 +0,0 @@
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
COIL_SYS_START_CMD,BOOL,,,,coil,8010,
COIL_SYS_STOP_CMD,BOOL,,,,coil,8020,
COIL_SYS_RESET_CMD,BOOL,,,,coil,8030,
COIL_SYS_HAND_CMD,BOOL,,,,coil,8040,
COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050,
COIL_SYS_INIT_CMD,BOOL,,,,coil,8060,
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700,
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd
COIL_SYS_START_STATUS,BOOL,,,,coil,8210,
COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220,
COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230,
COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240,
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250,
COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260,
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500,
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000,
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004,
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006,
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008,
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume
REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code()
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code()
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code()
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720,
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520,
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496,
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
REG_UNILAB_INTERACT,BOOL,,,,coil,8450,
,,,,,coil,8320,
COIL_ALUMINUM_FOIL,BOOL,,,,coil,8340,
REG_MSG_NE_PLATE_MATRIX,INT16,,,,hold_register,440,
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,,,hold_register,450,
REG_MSG_TIP_BOX_MATRIX,INT16,,,,hold_register,480,
REG_MSG_NE_PLATE_NUM,INT16,,,,hold_register,443,
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,,,hold_register,453,
REG_MSG_PRESS_MODE,BOOL,,,,coil,8360,
,BOOL,,,,coil,8300,
,BOOL,,,,coil,8310,
COIL_GB_L_IGNORE_CMD,BOOL,,,,coil,8320,
COIL_GB_R_IGNORE_CMD,BOOL,,,,coil,8420,
,BOOL,,,,coil,8350,
COIL_ELECTROLYTE_DUAL_DROP_MODE,BOOL,,,,coil,8370,
,BOOL,,,,coil,8380,
,BOOL,,,,coil,8390,
,BOOL,,,,coil,8400,
,BOOL,,,,coil,8410,
REG_MSG_DUAL_DROP_FIRST_VOLUME,INT16,,,,hold_register,4001,
COIL_DUAL_DROP_SUCTION_TIMING,BOOL,,,,coil,8430,
COIL_DUAL_DROP_START_TIMING,BOOL,,,,coil,8470,
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,,,coil,8460,
COIL_MATERIAL_SEARCH_DIALOG_APPEAR,BOOL,,,,coil,6470,
COIL_MATERIAL_SEARCH_CONFIRM_YES,BOOL,,,,coil,6480,
COIL_MATERIAL_SEARCH_CONFIRM_NO,BOOL,,,,coil,6490,
COIL_ALARM_100_SYSTEM_ERROR,BOOL,,,,coil,1000,异常100-系统异常
COIL_ALARM_101_EMERGENCY_STOP,BOOL,,,,coil,1010,异常101-急停
COIL_ALARM_111_GLOVEBOX_EMERGENCY_STOP,BOOL,,,,coil,1110,异常111-手套箱急停
COIL_ALARM_112_GLOVEBOX_GRATING_BLOCKED,BOOL,,,,coil,1120,异常112-手套箱内光栅遮挡
COIL_ALARM_160_PIPETTE_TIP_SHORTAGE,BOOL,,,,coil,1600,异常160-移液枪头缺料
COIL_ALARM_161_POSITIVE_SHELL_SHORTAGE,BOOL,,,,coil,1610,异常161-正极壳缺料
COIL_ALARM_162_ALUMINUM_FOIL_SHORTAGE,BOOL,,,,coil,1620,异常162-铝箔垫缺料
COIL_ALARM_163_POSITIVE_PLATE_SHORTAGE,BOOL,,,,coil,1630,异常163-正极片缺料
COIL_ALARM_164_SEPARATOR_SHORTAGE,BOOL,,,,coil,1640,异常164-隔膜缺料
COIL_ALARM_165_NEGATIVE_PLATE_SHORTAGE,BOOL,,,,coil,1650,异常165-负极片缺料
COIL_ALARM_166_FLAT_WASHER_SHORTAGE,BOOL,,,,coil,1660,异常166-平垫缺料
COIL_ALARM_167_SPRING_WASHER_SHORTAGE,BOOL,,,,coil,1670,异常167-弹垫缺料
COIL_ALARM_168_NEGATIVE_SHELL_SHORTAGE,BOOL,,,,coil,1680,异常168-负极壳缺料
COIL_ALARM_169_FINISHED_BATTERY_FULL,BOOL,,,,coil,1690,异常169-成品电池满料
COIL_ALARM_201_SERVO_AXIS_01_ERROR,BOOL,,,,coil,2010,异常201-伺服轴01异常
COIL_ALARM_202_SERVO_AXIS_02_ERROR,BOOL,,,,coil,2020,异常202-伺服轴02异常
COIL_ALARM_203_SERVO_AXIS_03_ERROR,BOOL,,,,coil,2030,异常203-伺服轴03异常
COIL_ALARM_204_SERVO_AXIS_04_ERROR,BOOL,,,,coil,2040,异常204-伺服轴04异常
COIL_ALARM_205_SERVO_AXIS_05_ERROR,BOOL,,,,coil,2050,异常205-伺服轴05异常
COIL_ALARM_206_SERVO_AXIS_06_ERROR,BOOL,,,,coil,2060,异常206-伺服轴06异常
COIL_ALARM_207_SERVO_AXIS_07_ERROR,BOOL,,,,coil,2070,异常207-伺服轴07异常
COIL_ALARM_208_SERVO_AXIS_08_ERROR,BOOL,,,,coil,2080,异常208-伺服轴08异常
COIL_ALARM_209_SERVO_AXIS_09_ERROR,BOOL,,,,coil,2090,异常209-伺服轴09异常
COIL_ALARM_210_SERVO_AXIS_10_ERROR,BOOL,,,,coil,2100,异常210-伺服轴10异常
COIL_ALARM_211_SERVO_AXIS_11_ERROR,BOOL,,,,coil,2110,异常211-伺服轴11异常
COIL_ALARM_212_SERVO_AXIS_12_ERROR,BOOL,,,,coil,2120,异常212-伺服轴12异常
COIL_ALARM_213_SERVO_AXIS_13_ERROR,BOOL,,,,coil,2130,异常213-伺服轴13异常
COIL_ALARM_214_SERVO_AXIS_14_ERROR,BOOL,,,,coil,2140,异常214-伺服轴14异常
COIL_ALARM_250_OTHER_COMPONENT_ERROR,BOOL,,,,coil,2500,异常250-其他元件异常
COIL_ALARM_251_PIPETTE_COMM_ERROR,BOOL,,,,coil,2510,异常251-移液枪通讯异常
COIL_ALARM_252_PIPETTE_ALARM,BOOL,,,,coil,2520,异常252-移液枪报警
COIL_ALARM_256_ELECTRIC_GRIPPER_ERROR,BOOL,,,,coil,2560,异常256-电爪异常
COIL_ALARM_262_RB_UNKNOWN_POSITION_ERROR,BOOL,,,,coil,2620,异常262-RB报警未知点位错误
COIL_ALARM_263_RB_XYZ_PARAM_LIMIT_ERROR,BOOL,,,,coil,2630,异常263-RB报警X、Y、Z参数超限制
COIL_ALARM_264_RB_VISION_PARAM_ERROR,BOOL,,,,coil,2640,异常264-RB报警视觉参数误差过大
COIL_ALARM_265_RB_NOZZLE_1_PICK_FAIL,BOOL,,,,coil,2650,异常265-RB报警1#吸嘴取料失败
COIL_ALARM_266_RB_NOZZLE_2_PICK_FAIL,BOOL,,,,coil,2660,异常266-RB报警2#吸嘴取料失败
COIL_ALARM_267_RB_NOZZLE_3_PICK_FAIL,BOOL,,,,coil,2670,异常267-RB报警3#吸嘴取料失败
COIL_ALARM_268_RB_NOZZLE_4_PICK_FAIL,BOOL,,,,coil,2680,异常268-RB报警4#吸嘴取料失败
COIL_ALARM_269_RB_TRAY_PICK_FAIL,BOOL,,,,coil,2690,异常269-RB报警取物料盘失败
COIL_ALARM_280_RB_COLLISION_ERROR,BOOL,,,,coil,2800,异常280-RB碰撞异常
COIL_ALARM_290_VISION_SYSTEM_COMM_ERROR,BOOL,,,,coil,2900,异常290-视觉系统通讯异常
COIL_ALARM_291_VISION_ALIGNMENT_NG,BOOL,,,,coil,2910,异常291-视觉对位NG异常
COIL_ALARM_292_BARCODE_SCANNER_COMM_ERROR,BOOL,,,,coil,2920,异常292-扫码枪通讯异常
COIL_ALARM_310_OCV_TRANSFER_NOZZLE_SUCTION_ERROR,BOOL,,,,coil,3100,异常310-开电移载吸嘴吸真空异常
COIL_ALARM_311_OCV_TRANSFER_NOZZLE_BREAK_ERROR,BOOL,,,,coil,3110,异常311-开电移载吸嘴破真空异常
COIL_ALARM_312_WEIGHT_TRANSFER_NOZZLE_SUCTION_ERROR,BOOL,,,,coil,3120,异常312-称重移载吸嘴吸真空异常
COIL_ALARM_313_WEIGHT_TRANSFER_NOZZLE_BREAK_ERROR,BOOL,,,,coil,3130,异常313-称重移载吸嘴破真空异常
COIL_ALARM_340_OCV_NOZZLE_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3400,异常340-开路电压吸嘴移载气缸异常
COIL_ALARM_342_OCV_NOZZLE_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3420,异常342-开路电压吸嘴升降气缸异常
COIL_ALARM_344_OCV_CRIMPING_CYLINDER_ERROR,BOOL,,,,coil,3440,异常344-开路电压旋压气缸异常
COIL_ALARM_350_WEIGHT_NOZZLE_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3500,异常350-称重吸嘴移载气缸异常
COIL_ALARM_352_WEIGHT_NOZZLE_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3520,异常352-称重吸嘴升降气缸异常
COIL_ALARM_354_CLEANING_CLOTH_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3540,异常354-清洗无尘布移载气缸异常
COIL_ALARM_356_CLEANING_CLOTH_PRESS_CYLINDER_ERROR,BOOL,,,,coil,3560,异常356-清洗无尘布压紧气缸异常
COIL_ALARM_360_ELECTROLYTE_BOTTLE_POSITION_CYLINDER_ERROR,BOOL,,,,coil,3600,异常360-电解液瓶定位气缸异常
COIL_ALARM_362_PIPETTE_TIP_BOX_POSITION_CYLINDER_ERROR,BOOL,,,,coil,3620,异常362-移液枪头盒定位气缸异常
COIL_ALARM_364_REAGENT_BOTTLE_GRIPPER_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3640,异常364-试剂瓶夹爪升降气缸异常
COIL_ALARM_366_REAGENT_BOTTLE_GRIPPER_CYLINDER_ERROR,BOOL,,,,coil,3660,异常366-试剂瓶夹爪气缸异常
COIL_ALARM_370_PRESS_MODULE_BLOW_CYLINDER_ERROR,BOOL,,,,coil,3700,异常370-压制模块吹气气缸异常
COIL_ALARM_151_ELECTROLYTE_BOTTLE_POSITION_ERROR,BOOL,,,,coil,1510,异常151-电解液瓶定位在籍异常
COIL_ALARM_152_ELECTROLYTE_BOTTLE_CAP_ERROR,BOOL,,,,coil,1520,异常152-电解液瓶盖在籍异常
1 Name DataType InitValue Comment Attribute DeviceType Address
2 COIL_SYS_START_CMD BOOL coil 8010
3 COIL_SYS_STOP_CMD BOOL coil 8020
4 COIL_SYS_RESET_CMD BOOL coil 8030
5 COIL_SYS_HAND_CMD BOOL coil 8040
6 COIL_SYS_AUTO_CMD BOOL coil 8050
7 COIL_SYS_INIT_CMD BOOL coil 8060
8 COIL_UNILAB_SEND_MSG_SUCC_CMD BOOL coil 8700
9 COIL_UNILAB_REC_MSG_SUCC_CMD BOOL coil 8710 unilab_rec_msg_succ_cmd
10 COIL_SYS_START_STATUS BOOL coil 8210
11 COIL_SYS_STOP_STATUS BOOL coil 8220
12 COIL_SYS_RESET_STATUS BOOL coil 8230
13 COIL_SYS_HAND_STATUS BOOL coil 8240
14 COIL_SYS_AUTO_STATUS BOOL coil 8250
15 COIL_SYS_INIT_STATUS BOOL coil 8260
16 COIL_REQUEST_REC_MSG_STATUS BOOL coil 8500
17 COIL_REQUEST_SEND_MSG_STATUS BOOL coil 8510 request_send_msg_status
18 REG_MSG_ELECTROLYTE_USE_NUM INT16 hold_register 11000
19 REG_MSG_ELECTROLYTE_NUM INT16 hold_register 11002 unilab_send_msg_electrolyte_num
20 REG_MSG_ELECTROLYTE_VOLUME INT16 hold_register 11004 unilab_send_msg_electrolyte_vol
21 REG_MSG_ASSEMBLY_TYPE INT16 hold_register 11006 unilab_send_msg_assembly_type
22 REG_MSG_ASSEMBLY_PRESSURE INT16 hold_register 11008 unilab_send_msg_assembly_pressure
23 REG_DATA_ASSEMBLY_COIN_CELL_NUM INT16 hold_register 10000 data_assembly_coin_cell_num
24 REG_DATA_OPEN_CIRCUIT_VOLTAGE FLOAT32 hold_register 10002 data_open_circuit_voltage
25 REG_DATA_AXIS_X_POS FLOAT32 hold_register 10004
26 REG_DATA_AXIS_Y_POS FLOAT32 hold_register 10006
27 REG_DATA_AXIS_Z_POS FLOAT32 hold_register 10008
28 REG_DATA_POLE_WEIGHT FLOAT32 hold_register 10010 data_pole_weight
29 REG_DATA_ASSEMBLY_PER_TIME FLOAT32 hold_register 10012 data_assembly_time
30 REG_DATA_ASSEMBLY_PRESSURE INT16 hold_register 10014 data_assembly_pressure
31 REG_DATA_ELECTROLYTE_VOLUME INT16 hold_register 10016 data_electrolyte_volume
32 REG_DATA_COIN_NUM INT16 hold_register 10018 data_coin_num
33 REG_DATA_ELECTROLYTE_CODE STRING hold_register 10020 data_electrolyte_code()
34 REG_DATA_COIN_CELL_CODE STRING hold_register 10030 data_coin_cell_code()
35 REG_DATA_STACK_VISON_CODE STRING hold_register 12004 data_stack_vision_code()
36 REG_DATA_GLOVE_BOX_PRESSURE FLOAT32 hold_register 10050 data_glove_box_pressure
37 REG_DATA_GLOVE_BOX_WATER_CONTENT FLOAT32 hold_register 10052 data_glove_box_water_content
38 REG_DATA_GLOVE_BOX_O2_CONTENT FLOAT32 hold_register 10054 data_glove_box_o2_content
39 UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM BOOL coil 8720
40 UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM BOOL coil 8520
41 REG_MSG_ELECTROLYTE_NUM_USED INT16 hold_register 496
42 REG_DATA_ELECTROLYTE_USE_NUM INT16 hold_register 10000
43 UNILAB_SEND_FINISHED_CMD BOOL coil 8730
44 UNILAB_RECE_FINISHED_CMD BOOL coil 8530
45 REG_DATA_ASSEMBLY_TYPE INT16 hold_register 10018 ASSEMBLY_TYPE7or8
46 REG_UNILAB_INTERACT BOOL coil 8450
47 coil 8320
48 COIL_ALUMINUM_FOIL BOOL coil 8340
49 REG_MSG_NE_PLATE_MATRIX INT16 hold_register 440
50 REG_MSG_SEPARATOR_PLATE_MATRIX INT16 hold_register 450
51 REG_MSG_TIP_BOX_MATRIX INT16 hold_register 480
52 REG_MSG_NE_PLATE_NUM INT16 hold_register 443
53 REG_MSG_SEPARATOR_PLATE_NUM INT16 hold_register 453
54 REG_MSG_PRESS_MODE BOOL coil 8360
55 BOOL coil 8300
56 BOOL coil 8310
57 COIL_GB_L_IGNORE_CMD BOOL coil 8320
58 COIL_GB_R_IGNORE_CMD BOOL coil 8420
59 BOOL coil 8350
60 COIL_ELECTROLYTE_DUAL_DROP_MODE BOOL coil 8370
61 BOOL coil 8380
62 BOOL coil 8390
63 BOOL coil 8400
64 BOOL coil 8410
65 REG_MSG_DUAL_DROP_FIRST_VOLUME INT16 hold_register 4001
66 COIL_DUAL_DROP_SUCTION_TIMING BOOL coil 8430
67 COIL_DUAL_DROP_START_TIMING BOOL coil 8470
68 REG_MSG_BATTERY_CLEAN_IGNORE BOOL coil 8460
69 COIL_MATERIAL_SEARCH_DIALOG_APPEAR BOOL coil 6470
70 COIL_MATERIAL_SEARCH_CONFIRM_YES BOOL coil 6480
71 COIL_MATERIAL_SEARCH_CONFIRM_NO BOOL coil 6490
72 COIL_ALARM_100_SYSTEM_ERROR BOOL coil 1000 异常100-系统异常
73 COIL_ALARM_101_EMERGENCY_STOP BOOL coil 1010 异常101-急停
74 COIL_ALARM_111_GLOVEBOX_EMERGENCY_STOP BOOL coil 1110 异常111-手套箱急停
75 COIL_ALARM_112_GLOVEBOX_GRATING_BLOCKED BOOL coil 1120 异常112-手套箱内光栅遮挡
76 COIL_ALARM_160_PIPETTE_TIP_SHORTAGE BOOL coil 1600 异常160-移液枪头缺料
77 COIL_ALARM_161_POSITIVE_SHELL_SHORTAGE BOOL coil 1610 异常161-正极壳缺料
78 COIL_ALARM_162_ALUMINUM_FOIL_SHORTAGE BOOL coil 1620 异常162-铝箔垫缺料
79 COIL_ALARM_163_POSITIVE_PLATE_SHORTAGE BOOL coil 1630 异常163-正极片缺料
80 COIL_ALARM_164_SEPARATOR_SHORTAGE BOOL coil 1640 异常164-隔膜缺料
81 COIL_ALARM_165_NEGATIVE_PLATE_SHORTAGE BOOL coil 1650 异常165-负极片缺料
82 COIL_ALARM_166_FLAT_WASHER_SHORTAGE BOOL coil 1660 异常166-平垫缺料
83 COIL_ALARM_167_SPRING_WASHER_SHORTAGE BOOL coil 1670 异常167-弹垫缺料
84 COIL_ALARM_168_NEGATIVE_SHELL_SHORTAGE BOOL coil 1680 异常168-负极壳缺料
85 COIL_ALARM_169_FINISHED_BATTERY_FULL BOOL coil 1690 异常169-成品电池满料
86 COIL_ALARM_201_SERVO_AXIS_01_ERROR BOOL coil 2010 异常201-伺服轴01异常
87 COIL_ALARM_202_SERVO_AXIS_02_ERROR BOOL coil 2020 异常202-伺服轴02异常
88 COIL_ALARM_203_SERVO_AXIS_03_ERROR BOOL coil 2030 异常203-伺服轴03异常
89 COIL_ALARM_204_SERVO_AXIS_04_ERROR BOOL coil 2040 异常204-伺服轴04异常
90 COIL_ALARM_205_SERVO_AXIS_05_ERROR BOOL coil 2050 异常205-伺服轴05异常
91 COIL_ALARM_206_SERVO_AXIS_06_ERROR BOOL coil 2060 异常206-伺服轴06异常
92 COIL_ALARM_207_SERVO_AXIS_07_ERROR BOOL coil 2070 异常207-伺服轴07异常
93 COIL_ALARM_208_SERVO_AXIS_08_ERROR BOOL coil 2080 异常208-伺服轴08异常
94 COIL_ALARM_209_SERVO_AXIS_09_ERROR BOOL coil 2090 异常209-伺服轴09异常
95 COIL_ALARM_210_SERVO_AXIS_10_ERROR BOOL coil 2100 异常210-伺服轴10异常
96 COIL_ALARM_211_SERVO_AXIS_11_ERROR BOOL coil 2110 异常211-伺服轴11异常
97 COIL_ALARM_212_SERVO_AXIS_12_ERROR BOOL coil 2120 异常212-伺服轴12异常
98 COIL_ALARM_213_SERVO_AXIS_13_ERROR BOOL coil 2130 异常213-伺服轴13异常
99 COIL_ALARM_214_SERVO_AXIS_14_ERROR BOOL coil 2140 异常214-伺服轴14异常
100 COIL_ALARM_250_OTHER_COMPONENT_ERROR BOOL coil 2500 异常250-其他元件异常
101 COIL_ALARM_251_PIPETTE_COMM_ERROR BOOL coil 2510 异常251-移液枪通讯异常
102 COIL_ALARM_252_PIPETTE_ALARM BOOL coil 2520 异常252-移液枪报警
103 COIL_ALARM_256_ELECTRIC_GRIPPER_ERROR BOOL coil 2560 异常256-电爪异常
104 COIL_ALARM_262_RB_UNKNOWN_POSITION_ERROR BOOL coil 2620 异常262-RB报警:未知点位错误
105 COIL_ALARM_263_RB_XYZ_PARAM_LIMIT_ERROR BOOL coil 2630 异常263-RB报警:X、Y、Z参数超限制
106 COIL_ALARM_264_RB_VISION_PARAM_ERROR BOOL coil 2640 异常264-RB报警:视觉参数误差过大
107 COIL_ALARM_265_RB_NOZZLE_1_PICK_FAIL BOOL coil 2650 异常265-RB报警:1#吸嘴取料失败
108 COIL_ALARM_266_RB_NOZZLE_2_PICK_FAIL BOOL coil 2660 异常266-RB报警:2#吸嘴取料失败
109 COIL_ALARM_267_RB_NOZZLE_3_PICK_FAIL BOOL coil 2670 异常267-RB报警:3#吸嘴取料失败
110 COIL_ALARM_268_RB_NOZZLE_4_PICK_FAIL BOOL coil 2680 异常268-RB报警:4#吸嘴取料失败
111 COIL_ALARM_269_RB_TRAY_PICK_FAIL BOOL coil 2690 异常269-RB报警:取物料盘失败
112 COIL_ALARM_280_RB_COLLISION_ERROR BOOL coil 2800 异常280-RB碰撞异常
113 COIL_ALARM_290_VISION_SYSTEM_COMM_ERROR BOOL coil 2900 异常290-视觉系统通讯异常
114 COIL_ALARM_291_VISION_ALIGNMENT_NG BOOL coil 2910 异常291-视觉对位NG异常
115 COIL_ALARM_292_BARCODE_SCANNER_COMM_ERROR BOOL coil 2920 异常292-扫码枪通讯异常
116 COIL_ALARM_310_OCV_TRANSFER_NOZZLE_SUCTION_ERROR BOOL coil 3100 异常310-开电移载吸嘴吸真空异常
117 COIL_ALARM_311_OCV_TRANSFER_NOZZLE_BREAK_ERROR BOOL coil 3110 异常311-开电移载吸嘴破真空异常
118 COIL_ALARM_312_WEIGHT_TRANSFER_NOZZLE_SUCTION_ERROR BOOL coil 3120 异常312-称重移载吸嘴吸真空异常
119 COIL_ALARM_313_WEIGHT_TRANSFER_NOZZLE_BREAK_ERROR BOOL coil 3130 异常313-称重移载吸嘴破真空异常
120 COIL_ALARM_340_OCV_NOZZLE_TRANSFER_CYLINDER_ERROR BOOL coil 3400 异常340-开路电压吸嘴移载气缸异常
121 COIL_ALARM_342_OCV_NOZZLE_LIFT_CYLINDER_ERROR BOOL coil 3420 异常342-开路电压吸嘴升降气缸异常
122 COIL_ALARM_344_OCV_CRIMPING_CYLINDER_ERROR BOOL coil 3440 异常344-开路电压旋压气缸异常
123 COIL_ALARM_350_WEIGHT_NOZZLE_TRANSFER_CYLINDER_ERROR BOOL coil 3500 异常350-称重吸嘴移载气缸异常
124 COIL_ALARM_352_WEIGHT_NOZZLE_LIFT_CYLINDER_ERROR BOOL coil 3520 异常352-称重吸嘴升降气缸异常
125 COIL_ALARM_354_CLEANING_CLOTH_TRANSFER_CYLINDER_ERROR BOOL coil 3540 异常354-清洗无尘布移载气缸异常
126 COIL_ALARM_356_CLEANING_CLOTH_PRESS_CYLINDER_ERROR BOOL coil 3560 异常356-清洗无尘布压紧气缸异常
127 COIL_ALARM_360_ELECTROLYTE_BOTTLE_POSITION_CYLINDER_ERROR BOOL coil 3600 异常360-电解液瓶定位气缸异常
128 COIL_ALARM_362_PIPETTE_TIP_BOX_POSITION_CYLINDER_ERROR BOOL coil 3620 异常362-移液枪头盒定位气缸异常
129 COIL_ALARM_364_REAGENT_BOTTLE_GRIPPER_LIFT_CYLINDER_ERROR BOOL coil 3640 异常364-试剂瓶夹爪升降气缸异常
130 COIL_ALARM_366_REAGENT_BOTTLE_GRIPPER_CYLINDER_ERROR BOOL coil 3660 异常366-试剂瓶夹爪气缸异常
131 COIL_ALARM_370_PRESS_MODULE_BLOW_CYLINDER_ERROR BOOL coil 3700 异常370-压制模块吹气气缸异常
132 COIL_ALARM_151_ELECTROLYTE_BOTTLE_POSITION_ERROR BOOL coil 1510 异常151-电解液瓶定位在籍异常
133 COIL_ALARM_152_ELECTROLYTE_BOTTLE_CAP_ERROR BOOL coil 1520 异常152-电解液瓶盖在籍异常

File diff suppressed because it is too large Load Diff

View File

@@ -459,12 +459,12 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
# 验证必需字段 # 验证必需字段
if 'brand' in request_data: if 'brand' in request_data:
if request_data['brand'] == "bioyond": # 奔曜 if request_data['brand'] == "bioyond": # 奔曜
material_data = request_data["text"] error_msg = request_data["text"]
logger.info(f"收到奔曜物料变更报送: {material_data}") logger.info(f"收到奔曜错误处理报送: {error_msg}")
return HttpResponse( return HttpResponse(
success=True, success=True,
message=f"物料变更报送已收到: {material_data}", message=f"错误处理报送已收到: {error_msg}",
acknowledgment_id=f"MATERIAL_{int(time.time() * 1000)}_{material_data.get('id', 'unknown')}", acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}",
data=None data=None
) )
else: else:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,658 @@
"""
装饰器注册表系统
通过 @device, @action, @resource 装饰器替代 YAML 配置文件来定义设备/动作/资源注册表信息。
Usage:
from unilabos.registry.decorators import (
device, action, resource,
InputHandle, OutputHandle,
ActionInputHandle, ActionOutputHandle,
HardwareInterface, Side, DataSource, NodeType,
)
@device(
id="solenoid_valve.mock",
category=["pump_and_valve"],
description="模拟电磁阀设备",
handles=[
InputHandle(key="in", data_type="fluid", label="in", side=Side.NORTH),
OutputHandle(key="out", data_type="fluid", label="out", side=Side.SOUTH),
],
hardware_interface=HardwareInterface(
name="hardware_interface",
read="send_command",
write="send_command",
),
)
class SolenoidValveMock:
@action(action_type=EmptyIn)
def close(self):
...
@action(
handles=[
ActionInputHandle(key="in", data_type="fluid", label="in"),
ActionOutputHandle(key="out", data_type="fluid", label="out"),
],
)
def set_valve_position(self, position):
...
# 无 @action 装饰器 => auto- 前缀动作
def is_open(self):
...
"""
from enum import Enum
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, TypeVar
from pydantic import BaseModel, ConfigDict, Field
F = TypeVar("F", bound=Callable[..., Any])
# ---------------------------------------------------------------------------
# 枚举
# ---------------------------------------------------------------------------
class Side(str, Enum):
"""UI 上 Handle 的显示位置"""
NORTH = "NORTH"
SOUTH = "SOUTH"
EAST = "EAST"
WEST = "WEST"
class DataSource(str, Enum):
"""Handle 的数据来源"""
HANDLE = "handle" # 从上游 handle 获取数据 (用于 InputHandle)
EXECUTOR = "executor" # 从执行器输出数据 (用于 OutputHandle)
class NodeType(str, Enum):
"""动作的节点类型(用于区分 ILab 节点和人工确认节点等)"""
ILAB = "ILab"
MANUAL_CONFIRM = "manual_confirm"
# ---------------------------------------------------------------------------
# Device / Resource Handle (设备/资源级别端口, 序列化时包含 io_type)
# ---------------------------------------------------------------------------
class _DeviceHandleBase(BaseModel):
"""设备/资源端口基类 (内部使用)"""
model_config = ConfigDict(populate_by_name=True)
key: str = Field(serialization_alias="handler_key")
data_type: str
label: str
side: Optional[Side] = None
data_key: Optional[str] = None
data_source: Optional[str] = None
description: Optional[str] = None
# 子类覆盖
io_type: str = ""
def to_registry_dict(self) -> Dict[str, Any]:
return self.model_dump(by_alias=True, exclude_none=True)
class InputHandle(_DeviceHandleBase):
"""
输入端口 (io_type="target"), 用于 @device / @resource handles
Example:
InputHandle(key="in", data_type="fluid", label="in", side=Side.NORTH)
"""
io_type: str = "target"
class OutputHandle(_DeviceHandleBase):
"""
输出端口 (io_type="source"), 用于 @device / @resource handles
Example:
OutputHandle(key="out", data_type="fluid", label="out", side=Side.SOUTH)
"""
io_type: str = "source"
# ---------------------------------------------------------------------------
# Action Handle (动作级别端口, 序列化时不含 io_type, 按类型自动分组)
# ---------------------------------------------------------------------------
class _ActionHandleBase(BaseModel):
"""动作端口基类 (内部使用)"""
model_config = ConfigDict(populate_by_name=True)
key: str = Field(serialization_alias="handler_key")
data_type: str
label: str
side: Optional[Side] = None
data_key: Optional[str] = None
data_source: Optional[str] = None
description: Optional[str] = None
io_type: Optional[str] = None # source/sink (dataflow) or target/source (device-style)
def to_registry_dict(self) -> Dict[str, Any]:
return self.model_dump(by_alias=True, exclude_none=True)
class ActionInputHandle(_ActionHandleBase):
"""
动作输入端口, 用于 @action handles, 序列化后归入 "input"
Example:
ActionInputHandle(
key="material_input", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source="handle",
)
"""
pass
class ActionOutputHandle(_ActionHandleBase):
"""
动作输出端口, 用于 @action handles, 序列化后归入 "output"
Example:
ActionOutputHandle(
key="station_output", data_type="workbench_station",
label="加热台ID", data_key="station_id", data_source="executor",
)
"""
pass
# ---------------------------------------------------------------------------
# HardwareInterface
# ---------------------------------------------------------------------------
class HardwareInterface(BaseModel):
"""
硬件通信接口定义
描述设备与底层硬件通信的方式 (串口、Modbus 等)。
Example:
HardwareInterface(name="hardware_interface", read="send_command", write="send_command")
"""
name: str
read: Optional[str] = None
write: Optional[str] = None
extra_info: Optional[List[str]] = None
# ---------------------------------------------------------------------------
# 全局注册表 -- 记录所有被装饰器标记的类/函数
# ---------------------------------------------------------------------------
_registered_devices: Dict[str, type] = {} # device_id -> class
_registered_resources: Dict[str, Any] = {} # resource_id -> class or function
def _device_handles_to_list(
handles: Optional[List[_DeviceHandleBase]],
) -> List[Dict[str, Any]]:
"""将设备/资源 Handle 列表序列化为字典列表 (含 io_type)"""
if handles is None:
return []
return [h.to_registry_dict() for h in handles]
def _action_handles_to_dict(
handles: Optional[List[_ActionHandleBase]],
) -> Dict[str, Any]:
"""
将动作 Handle 列表序列化为 {"input": [...], "output": [...]} 格式。
ActionInputHandle => "input", ActionOutputHandle => "output"
"""
if handles is None:
return {}
input_list = [h.to_registry_dict() for h in handles if isinstance(h, ActionInputHandle)]
output_list = [h.to_registry_dict() for h in handles if isinstance(h, ActionOutputHandle)]
result: Dict[str, Any] = {}
if input_list:
result["input"] = input_list
if output_list:
result["output"] = output_list
return result
# ---------------------------------------------------------------------------
# @device 类装饰器
# ---------------------------------------------------------------------------
# noinspection PyShadowingBuiltins
def device(
id: Optional[str] = None,
ids: Optional[List[str]] = None,
id_meta: Optional[Dict[str, Dict[str, Any]]] = None,
category: Optional[List[str]] = None,
description: str = "",
display_name: str = "",
icon: str = "",
version: str = "1.0.0",
handles: Optional[List[_DeviceHandleBase]] = None,
model: Optional[Dict[str, Any]] = None,
device_type: str = "python",
hardware_interface: Optional[HardwareInterface] = None,
):
"""
设备类装饰器
将类标记为一个 UniLab-OS 设备,并附加注册表元数据。
支持两种模式:
1. 单设备: id="xxx", category=[...]
2. 多设备: ids=["id1","id2"], id_meta={"id1":{handles:[...]}, "id2":{...}}
Args:
id: 单设备时的注册表唯一标识
ids: 多设备时的 id 列表,与 id_meta 配合使用
id_meta: 每个 device_id 的覆盖元数据 (handles/description/icon/model)
category: 设备分类标签列表 (必填)
description: 设备描述
display_name: 人类可读的设备显示名称,缺失时默认使用 id
icon: 图标路径
version: 版本号
handles: 设备端口列表 (单设备或 id_meta 未覆盖时使用)
model: 可选的 3D 模型配置
device_type: 设备实现类型 ("python" / "ros2")
hardware_interface: 硬件通信接口 (HardwareInterface)
"""
# Resolve device ids
if ids is not None:
device_ids = list(ids)
if not device_ids:
raise ValueError("@device ids 不能为空")
id_meta = id_meta or {}
elif id is not None:
device_ids = [id]
id_meta = {}
else:
raise ValueError("@device 必须提供 id 或 ids")
if category is None:
raise ValueError("@device category 必填")
base_meta = {
"category": category,
"description": description,
"display_name": display_name,
"icon": icon,
"version": version,
"handles": _device_handles_to_list(handles),
"model": model,
"device_type": device_type,
"hardware_interface": (hardware_interface.model_dump(exclude_none=True) if hardware_interface else None),
}
def decorator(cls):
cls._device_registry_meta = base_meta
cls._device_registry_id_meta = id_meta
cls._device_registry_ids = device_ids
for did in device_ids:
if did in _registered_devices:
raise ValueError(f"@device id 重复: '{did}' 已被 {_registered_devices[did]} 注册")
_registered_devices[did] = cls
return cls
return decorator
# ---------------------------------------------------------------------------
# @action 方法装饰器
# ---------------------------------------------------------------------------
# 区分 "用户没传 action_type" 和 "用户传了 None"
_ACTION_TYPE_UNSET = object()
# noinspection PyShadowingNames
def action(
action_type: Any = _ACTION_TYPE_UNSET,
goal: Optional[Dict[str, str]] = None,
feedback: Optional[Dict[str, str]] = None,
result: Optional[Dict[str, str]] = None,
handles: Optional[List[_ActionHandleBase]] = None,
goal_default: Optional[Dict[str, Any]] = None,
placeholder_keys: Optional[Dict[str, str]] = None,
always_free: bool = False,
is_protocol: bool = False,
description: str = "",
auto_prefix: bool = False,
parent: bool = False,
node_type: Optional["NodeType"] = None,
):
"""
动作方法装饰器
标记方法为注册表动作。有三种用法:
1. @action(action_type=EmptyIn, ...) -- 非 auto, 使用指定 ROS Action 类型
2. @action() -- 非 auto, UniLabJsonCommand (从方法签名生成 schema)
3. 不加 @action -- auto- 前缀, UniLabJsonCommand
Protocol 用法:
@action(action_type=Add, is_protocol=True)
def AddProtocol(self): ...
标记该动作为高级协议 (protocol),运行时通过 ROS Action 路由到
protocol generator 执行。action_type 指向 unilabos_msgs 的 Action 类型。
Args:
action_type: ROS Action 消息类型 (如 EmptyIn, SendCmd, HeatChill).
不传/默认 = UniLabJsonCommand (非 auto).
goal: Goal 字段映射 (ROS字段名 -> 设备参数名).
protocol 模式下可留空,系统自动生成 identity 映射.
feedback: Feedback 字段映射
result: Result 字段映射
handles: 动作端口列表 (ActionInputHandle / ActionOutputHandle)
goal_default: Goal 字段默认值映射 (字段名 -> 默认值), 与自动生成的 goal_default 合并
placeholder_keys: 参数占位符配置
always_free: 是否为永久闲置动作 (不受排队限制)
is_protocol: 是否为工作站协议 (protocol)。True 时运行时走 protocol generator 路径。
description: 动作描述
auto_prefix: 若为 True动作名使用 auto-{method_name} 形式(与无 @action 时一致)
parent: 若为 True当方法参数为空 (*args, **kwargs) 时,通过 MRO 从父类获取真实方法参数
node_type: 动作的节点类型 (NodeType.ILAB / NodeType.MANUAL_CONFIRM)。
不填写时不写入注册表。
"""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# action_type 为哨兵值 => 用户没传, 视为 None (UniLabJsonCommand)
resolved_type = None if action_type is _ACTION_TYPE_UNSET else action_type
meta = {
"action_type": resolved_type,
"goal": goal or {},
"feedback": feedback or {},
"result": result or {},
"handles": _action_handles_to_dict(handles),
"goal_default": goal_default or {},
"placeholder_keys": placeholder_keys or {},
"always_free": always_free,
"is_protocol": is_protocol,
"description": description,
"auto_prefix": auto_prefix,
"parent": parent,
}
if node_type is not None:
meta["node_type"] = node_type.value if isinstance(node_type, NodeType) else str(node_type)
wrapper._action_registry_meta = meta # type: ignore[attr-defined]
# 设置 _is_always_free 保持与旧 @always_free 装饰器兼容
if always_free:
wrapper._is_always_free = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
def get_action_meta(func) -> Optional[Dict[str, Any]]:
"""获取方法上的 @action 装饰器元数据"""
return getattr(func, "_action_registry_meta", None)
def has_action_decorator(func) -> bool:
"""检查函数是否带有 @action 装饰器"""
return hasattr(func, "_action_registry_meta")
# ---------------------------------------------------------------------------
# @resource 类/函数装饰器
# ---------------------------------------------------------------------------
def resource(
id: str,
category: List[str],
description: str = "",
icon: str = "",
version: str = "1.0.0",
handles: Optional[List[_DeviceHandleBase]] = None,
model: Optional[Dict[str, Any]] = None,
class_type: str = "pylabrobot",
):
"""
资源类/函数装饰器
将类或工厂函数标记为一个 UniLab-OS 资源,附加注册表元数据。
Args:
id: 注册表唯一标识 (必填, 不可重复)
category: 资源分类标签列表 (必填)
description: 资源描述
icon: 图标路径
version: 版本号
handles: 端口列表 (InputHandle / OutputHandle)
model: 可选的 3D 模型配置
class_type: 资源实现类型 ("python" / "pylabrobot" / "unilabos")
"""
def decorator(obj):
meta = {
"resource_id": id,
"category": category,
"description": description,
"icon": icon,
"version": version,
"handles": _device_handles_to_list(handles),
"model": model,
"class_type": class_type,
}
obj._resource_registry_meta = meta
if id in _registered_resources:
raise ValueError(f"@resource id 重复: '{id}' 已被 {_registered_resources[id]} 注册")
_registered_resources[id] = obj
return obj
return decorator
def get_device_meta(cls, device_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
获取类上的 @device 装饰器元数据。
当 device_id 存在且类使用 ids+id_meta 时,返回合并后的 meta
(base_meta 与 id_meta[device_id] 深度合并)。
"""
base = getattr(cls, "_device_registry_meta", None)
if base is None:
return None
id_meta = getattr(cls, "_device_registry_id_meta", None) or {}
if device_id is None or device_id not in id_meta:
result = dict(base)
ids = getattr(cls, "_device_registry_ids", None)
result["device_id"] = device_id if device_id is not None else (ids[0] if ids else None)
return result
overrides = id_meta[device_id]
result = dict(base)
result["device_id"] = device_id
for key in ["handles", "description", "icon", "model"]:
if key in overrides:
val = overrides[key]
if key == "handles" and isinstance(val, list):
# handles 必须是 Handle 对象列表
result[key] = [h.to_registry_dict() for h in val]
else:
result[key] = val
return result
def get_resource_meta(obj) -> Optional[Dict[str, Any]]:
"""获取对象上的 @resource 装饰器元数据"""
return getattr(obj, "_resource_registry_meta", None)
def get_all_registered_devices() -> Dict[str, type]:
"""获取所有已注册的设备类"""
return _registered_devices.copy()
def get_all_registered_resources() -> Dict[str, Any]:
"""获取所有已注册的资源"""
return _registered_resources.copy()
def clear_registry():
"""清空全局注册表 (用于测试)"""
_registered_devices.clear()
_registered_resources.clear()
# ---------------------------------------------------------------------------
# 枚举值归一化
# ---------------------------------------------------------------------------
def normalize_enum_value(raw: Any, enum_cls) -> Optional[str]:
"""将 AST 提取的枚举成员名 / YAML 值字符串 / 旧格式长路径统一归一化为枚举值。
适用于 Side、DataSource、NodeType 等继承自 ``str, Enum`` 的装饰器枚举。
处理以下格式:
- "MANUAL_CONFIRM" → NodeType["MANUAL_CONFIRM"].value = "manual_confirm"
- "manual_confirm" → NodeType("manual_confirm").value = "manual_confirm"
- "HANDLE" → DataSource["HANDLE"].value = "handle"
- "NORTH" → Side["NORTH"].value = "NORTH"
- 旧缓存长路径 "unilabos...NodeType.MANUAL_CONFIRM" → 先 rsplit 再查找
"""
if not raw:
return None
raw_str = str(raw)
if "." in raw_str:
raw_str = raw_str.rsplit(".", 1)[-1]
try:
return enum_cls[raw_str].value
except KeyError:
pass
try:
return enum_cls(raw_str).value
except ValueError:
return raw_str
# ---------------------------------------------------------------------------
# topic_config / not_action / always_free 装饰器
# ---------------------------------------------------------------------------
def topic_config(
period: Optional[float] = None,
print_publish: Optional[bool] = None,
qos: Optional[int] = None,
name: Optional[str] = None,
) -> Callable[[F], F]:
"""
Topic发布配置装饰器
用于装饰 get_{attr_name} 方法或 @property控制对应属性的ROS topic发布行为。
Args:
period: 发布周期。None 表示使用默认值 5.0
print_publish: 是否打印发布日志。None 表示使用节点默认配置
qos: QoS深度配置。None 表示使用默认值 10
name: 自定义发布名称。None 表示使用方法名(去掉 get_ 前缀)
Note:
与 @property 连用时,@topic_config 必须放在 @property 下面,
这样装饰器执行顺序为:先 topic_config 添加配置,再 property 包装。
"""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper._topic_period = period # type: ignore[attr-defined]
wrapper._topic_print_publish = print_publish # type: ignore[attr-defined]
wrapper._topic_qos = qos # type: ignore[attr-defined]
wrapper._topic_name = name # type: ignore[attr-defined]
wrapper._has_topic_config = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
def get_topic_config(func) -> dict:
"""获取函数上的 topic 配置 (period, print_publish, qos, name)"""
if hasattr(func, "_has_topic_config") and getattr(func, "_has_topic_config", False):
return {
"period": getattr(func, "_topic_period", None),
"print_publish": getattr(func, "_topic_print_publish", None),
"qos": getattr(func, "_topic_qos", None),
"name": getattr(func, "_topic_name", None),
}
return {}
def always_free(func: F) -> F:
"""
标记动作为永久闲置(不受busy队列限制)的装饰器
被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制,
任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。
"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper._is_always_free = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
def is_always_free(func) -> bool:
"""检查函数是否被标记为永久闲置"""
return getattr(func, "_is_always_free", False)
def not_action(func: F) -> F:
"""
标记方法为非动作的装饰器
用于装饰 driver 类中的方法,使其在注册表扫描时不被识别为动作。
适用于辅助方法、内部工具方法等不应暴露为设备动作的公共方法。
"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper._is_not_action = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
def is_not_action(func) -> bool:
"""检查函数是否被标记为非动作"""
return getattr(func, "_is_not_action", False)

View File

@@ -96,10 +96,13 @@ serial:
type: string type: string
port: port:
type: string type: string
registry_name:
type: string
resource_tracker: resource_tracker:
type: object type: object
required: required:
- device_id - device_id
- registry_name
- port - port
type: object type: object
data: data:

View File

@@ -13,21 +13,18 @@ Qone_nmr:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -71,31 +68,6 @@ Qone_nmr:
title: monitor_folder_for_new_content参数 title: monitor_folder_for_new_content参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-strings_to_txt: auto-strings_to_txt:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -138,21 +110,18 @@ Qone_nmr:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -167,32 +136,31 @@ Qone_nmr:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:

View File

@@ -0,0 +1,589 @@
workstation.bioyond_dispensing_station:
category:
- workstation
- bioyond
class:
action_value_mappings:
auto-batch_create_90_10_vial_feeding_tasks:
feedback: {}
goal: {}
goal_default:
delay_time: null
hold_m_name: null
liquid_material_name: NMP
speed: null
temperature: null
titration: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
delay_time:
type: string
hold_m_name:
type: string
liquid_material_name:
default: NMP
type: string
speed:
type: string
temperature:
type: string
titration:
type: string
required:
- titration
type: object
result: {}
required:
- goal
title: batch_create_90_10_vial_feeding_tasks参数
type: object
type: UniLabJsonCommand
auto-batch_create_diamine_solution_tasks:
feedback: {}
goal: {}
goal_default:
delay_time: null
liquid_material_name: NMP
solutions: null
speed: null
temperature: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
delay_time:
type: string
liquid_material_name:
default: NMP
type: string
solutions:
type: string
speed:
type: string
temperature:
type: string
required:
- solutions
type: object
result: {}
required:
- goal
title: batch_create_diamine_solution_tasks参数
type: object
type: UniLabJsonCommand
auto-brief_step_parameters:
feedback: {}
goal: {}
goal_default:
data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
data:
type: object
required:
- data
type: object
result: {}
required:
- goal
title: brief_step_parameters参数
type: object
type: UniLabJsonCommand
auto-compute_experiment_design:
feedback: {}
goal: {}
goal_default:
m_tot: '70'
ratio: null
titration_percent: '0.03'
wt_percent: '0.25'
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
m_tot:
default: '70'
type: string
ratio:
type: object
titration_percent:
default: '0.03'
type: string
wt_percent:
default: '0.25'
type: string
required:
- ratio
type: object
result:
properties:
feeding_order:
items: {}
title: Feeding Order
type: array
return_info:
title: Return Info
type: string
solutions:
items: {}
title: Solutions
type: array
solvents:
additionalProperties: true
title: Solvents
type: object
titration:
additionalProperties: true
title: Titration
type: object
required:
- solutions
- titration
- solvents
- feeding_order
- return_info
title: ComputeExperimentDesignReturn
type: object
required:
- goal
title: compute_experiment_design参数
type: object
type: UniLabJsonCommand
auto-process_order_finish_report:
feedback: {}
goal: {}
goal_default:
report_request: null
used_materials: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
report_request:
type: string
used_materials:
type: string
required:
- report_request
- used_materials
type: object
result: {}
required:
- goal
title: process_order_finish_report参数
type: object
type: UniLabJsonCommand
auto-project_order_report:
feedback: {}
goal: {}
goal_default:
order_id: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
order_id:
type: string
required:
- order_id
type: object
result: {}
required:
- goal
title: project_order_report参数
type: object
type: UniLabJsonCommand
auto-query_resource_by_name:
feedback: {}
goal: {}
goal_default:
material_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
type: string
required:
- material_name
type: object
result: {}
required:
- goal
title: query_resource_by_name参数
type: object
type: UniLabJsonCommand
auto-transfer_materials_to_reaction_station:
feedback: {}
goal: {}
goal_default:
target_device_id: null
transfer_groups: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
target_device_id:
type: string
transfer_groups:
type: array
required:
- target_device_id
- transfer_groups
type: object
result: {}
required:
- goal
title: transfer_materials_to_reaction_station参数
type: object
type: UniLabJsonCommand
auto-wait_for_multiple_orders_and_get_reports:
feedback: {}
goal: {}
goal_default:
batch_create_result: null
check_interval: 10
timeout: 7200
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
batch_create_result:
type: string
check_interval:
default: 10
type: integer
timeout:
default: 7200
type: integer
required: []
type: object
result: {}
required:
- goal
title: wait_for_multiple_orders_and_get_reports参数
type: object
type: UniLabJsonCommand
auto-workflow_sample_locations:
feedback: {}
goal: {}
goal_default:
workflow_id: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
workflow_id:
type: string
required:
- workflow_id
type: object
result: {}
required:
- goal
title: workflow_sample_locations参数
type: object
type: UniLabJsonCommand
create_90_10_vial_feeding_task:
feedback: {}
goal:
delay_time: delay_time
hold_m_name: hold_m_name
order_name: order_name
percent_10_1_assign_material_name: percent_10_1_assign_material_name
percent_10_1_liquid_material_name: percent_10_1_liquid_material_name
percent_10_1_target_weigh: percent_10_1_target_weigh
percent_10_1_volume: percent_10_1_volume
percent_10_2_assign_material_name: percent_10_2_assign_material_name
percent_10_2_liquid_material_name: percent_10_2_liquid_material_name
percent_10_2_target_weigh: percent_10_2_target_weigh
percent_10_2_volume: percent_10_2_volume
percent_10_3_assign_material_name: percent_10_3_assign_material_name
percent_10_3_liquid_material_name: percent_10_3_liquid_material_name
percent_10_3_target_weigh: percent_10_3_target_weigh
percent_10_3_volume: percent_10_3_volume
percent_90_1_assign_material_name: percent_90_1_assign_material_name
percent_90_1_target_weigh: percent_90_1_target_weigh
percent_90_2_assign_material_name: percent_90_2_assign_material_name
percent_90_2_target_weigh: percent_90_2_target_weigh
percent_90_3_assign_material_name: percent_90_3_assign_material_name
percent_90_3_target_weigh: percent_90_3_target_weigh
speed: speed
temperature: temperature
goal_default:
delay_time: ''
hold_m_name: ''
order_name: ''
percent_10_1_assign_material_name: ''
percent_10_1_liquid_material_name: ''
percent_10_1_target_weigh: ''
percent_10_1_volume: ''
percent_10_2_assign_material_name: ''
percent_10_2_liquid_material_name: ''
percent_10_2_target_weigh: ''
percent_10_2_volume: ''
percent_10_3_assign_material_name: ''
percent_10_3_liquid_material_name: ''
percent_10_3_target_weigh: ''
percent_10_3_volume: ''
percent_90_1_assign_material_name: ''
percent_90_1_target_weigh: ''
percent_90_2_assign_material_name: ''
percent_90_2_target_weigh: ''
percent_90_3_assign_material_name: ''
percent_90_3_target_weigh: ''
speed: ''
temperature: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: DispenStationVialFeed_Feedback
type: object
goal:
properties:
delay_time:
type: string
hold_m_name:
type: string
order_name:
type: string
percent_10_1_assign_material_name:
type: string
percent_10_1_liquid_material_name:
type: string
percent_10_1_target_weigh:
type: string
percent_10_1_volume:
type: string
percent_10_2_assign_material_name:
type: string
percent_10_2_liquid_material_name:
type: string
percent_10_2_target_weigh:
type: string
percent_10_2_volume:
type: string
percent_10_3_assign_material_name:
type: string
percent_10_3_liquid_material_name:
type: string
percent_10_3_target_weigh:
type: string
percent_10_3_volume:
type: string
percent_90_1_assign_material_name:
type: string
percent_90_1_target_weigh:
type: string
percent_90_2_assign_material_name:
type: string
percent_90_2_target_weigh:
type: string
percent_90_3_assign_material_name:
type: string
percent_90_3_target_weigh:
type: string
speed:
type: string
temperature:
type: string
required:
- order_name
- percent_90_1_assign_material_name
- percent_90_1_target_weigh
- percent_90_2_assign_material_name
- percent_90_2_target_weigh
- percent_90_3_assign_material_name
- percent_90_3_target_weigh
- percent_10_1_assign_material_name
- percent_10_1_target_weigh
- percent_10_1_volume
- percent_10_1_liquid_material_name
- percent_10_2_assign_material_name
- percent_10_2_target_weigh
- percent_10_2_volume
- percent_10_2_liquid_material_name
- percent_10_3_assign_material_name
- percent_10_3_target_weigh
- percent_10_3_volume
- percent_10_3_liquid_material_name
- speed
- temperature
- delay_time
- hold_m_name
title: DispenStationVialFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: DispenStationVialFeed_Result
type: object
required:
- goal
title: DispenStationVialFeed
type: object
type: DispenStationVialFeed
create_diamine_solution_task:
feedback: {}
goal:
delay_time: delay_time
hold_m_name: hold_m_name
liquid_material_name: liquid_material_name
material_name: material_name
order_name: order_name
speed: speed
target_weigh: target_weigh
temperature: temperature
volume: volume
goal_default:
delay_time: ''
hold_m_name: ''
liquid_material_name: ''
material_name: ''
order_name: ''
speed: ''
target_weigh: ''
temperature: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: DispenStationSolnPrep_Feedback
type: object
goal:
properties:
delay_time:
type: string
hold_m_name:
type: string
liquid_material_name:
type: string
material_name:
type: string
order_name:
type: string
speed:
type: string
target_weigh:
type: string
temperature:
type: string
volume:
type: string
required:
- order_name
- material_name
- target_weigh
- volume
- liquid_material_name
- speed
- temperature
- delay_time
- hold_m_name
title: DispenStationSolnPrep_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: DispenStationSolnPrep_Result
type: object
required:
- goal
title: DispenStationSolnPrep
type: object
type: DispenStationSolnPrep
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
status_types: {}
type: python
config_info: []
description: ''
handles: []
icon: ''
init_param_schema:
config:
properties:
config:
type: string
deck:
type: string
required:
- config
- deck
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -22,7 +22,8 @@ bioyond_cell:
required: required:
- xlsx_path - xlsx_path
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: auto_batch_outbound_from_xlsx参数 title: auto_batch_outbound_from_xlsx参数
@@ -490,7 +491,9 @@ bioyond_cell:
goal: goal:
properties: properties:
material_names: material_names:
type: string items:
type: string
type: array
type_id: type_id:
default: 3a190ca0-b2f6-9aeb-8067-547e72c11469 default: 3a190ca0-b2f6-9aeb-8067-547e72c11469
type: string type: string
@@ -499,7 +502,8 @@ bioyond_cell:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_and_inbound_materials参数 title: create_and_inbound_materials参数
@@ -535,7 +539,8 @@ bioyond_cell:
- type_id - type_id
- warehouse_name - warehouse_name
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_material参数 title: create_material参数
@@ -556,11 +561,16 @@ bioyond_cell:
goal: goal:
properties: properties:
mappings: mappings:
additionalProperties:
type: object
type: object type: object
required: required:
- mappings - mappings
type: object type: object
result: {} result:
items:
type: object
type: array
required: required:
- goal - goal
title: create_materials参数 title: create_materials参数
@@ -592,7 +602,8 @@ bioyond_cell:
required: required:
- xlsx_path - xlsx_path
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_orders参数 title: create_orders参数
@@ -624,7 +635,8 @@ bioyond_cell:
required: required:
- xlsx_path - xlsx_path
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_orders_v2参数 title: create_orders_v2参数
@@ -665,7 +677,8 @@ bioyond_cell:
- bottle_type - bottle_type
- location_code - location_code
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_sample参数 title: create_sample参数
@@ -718,7 +731,8 @@ bioyond_cell:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: order_list_v2参数 title: order_list_v2参数
@@ -821,7 +835,8 @@ bioyond_cell:
required: required:
- material_obj - material_obj
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: report_material_change参数 title: report_material_change参数
@@ -875,7 +890,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_continue参数 title: scheduler_continue参数
@@ -896,7 +912,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_reset参数 title: scheduler_reset参数
@@ -917,7 +934,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_start参数 title: scheduler_start参数
@@ -1362,7 +1380,8 @@ bioyond_cell:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_start_and_auto_feeding参数 title: scheduler_start_and_auto_feeding参数
@@ -1807,7 +1826,8 @@ bioyond_cell:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_start_and_auto_feeding_v2参数 title: scheduler_start_and_auto_feeding_v2参数
@@ -1828,7 +1848,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_stop参数 title: scheduler_stop参数
@@ -1850,12 +1871,15 @@ bioyond_cell:
properties: properties:
items: items:
items: items:
additionalProperties:
type: string
type: object type: object
type: array type: array
required: required:
- items - items
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: storage_batch_inbound参数 title: storage_batch_inbound参数
@@ -1884,7 +1908,8 @@ bioyond_cell:
- material_id - material_id
- location_id - location_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: storage_inbound参数 title: storage_inbound参数
@@ -1905,7 +1930,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: transfer_1_to_2参数 title: transfer_1_to_2参数
@@ -1946,7 +1972,8 @@ bioyond_cell:
type: integer type: integer
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: transfer_3_to_2参数 title: transfer_3_to_2参数
@@ -1983,7 +2010,8 @@ bioyond_cell:
type: integer type: integer
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: transfer_3_to_2_to_1参数 title: transfer_3_to_2_to_1参数
@@ -2007,10 +2035,11 @@ bioyond_cell:
ip: ip:
type: string type: string
port: port:
type: string type: integer
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: update_push_ip参数 title: update_push_ip参数
@@ -2039,7 +2068,8 @@ bioyond_cell:
required: required:
- order_code - order_code
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: wait_for_order_finish参数 title: wait_for_order_finish参数
@@ -2072,7 +2102,8 @@ bioyond_cell:
required: required:
- order_code - order_code
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: wait_for_order_finish_polling参数 title: wait_for_order_finish_polling参数
@@ -2104,7 +2135,8 @@ bioyond_cell:
type: integer type: integer
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: wait_for_transfer_task参数 title: wait_for_transfer_task参数
@@ -2112,8 +2144,7 @@ bioyond_cell:
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation
status_types: status_types:
device_id: String device_id: ''
material_info: dict
type: python type: python
config_info: [] config_info: []
description: '' description: ''
@@ -2134,11 +2165,7 @@ bioyond_cell:
properties: properties:
device_id: device_id:
type: string type: string
material_info:
type: object
required: required:
- device_id - device_id
- material_info
type: object type: object
registry_type: device
version: 1.0.0 version: 1.0.0

View File

@@ -24,7 +24,8 @@ bioyond_dispensing_station:
required: required:
- data - data
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: brief_step_parameters参数 title: brief_step_parameters参数
@@ -53,7 +54,8 @@ bioyond_dispensing_station:
- report_request - report_request
- used_materials - used_materials
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: process_order_finish_report参数 title: process_order_finish_report参数
@@ -78,7 +80,8 @@ bioyond_dispensing_station:
required: required:
- order_id - order_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: project_order_report参数 title: project_order_report参数
@@ -128,7 +131,8 @@ bioyond_dispensing_station:
required: required:
- workflow_id - workflow_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: workflow_sample_locations参数 title: workflow_sample_locations参数
@@ -144,12 +148,12 @@ bioyond_dispensing_station:
temperature: temperature temperature: temperature
titration: titration titration: titration
goal_default: goal_default:
delay_time: '600' delay_time: null
hold_m_name: '' hold_m_name: null
liquid_material_name: NMP liquid_material_name: NMP
speed: '400' speed: null
temperature: '40' temperature: null
titration: '' titration: null
handles: handles:
input: input:
- data_key: titration - data_key: titration
@@ -165,20 +169,16 @@ bioyond_dispensing_station:
handler_key: BATCH_CREATE_RESULT handler_key: BATCH_CREATE_RESULT
io_type: sink io_type: sink
label: Complete Batch Create Result JSON (contains order_codes and order_ids) label: Complete Batch Create Result JSON (contains order_codes and order_ids)
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。 description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。
properties: properties:
feedback: feedback:
properties: {}
required: []
title: BatchCreate9010VialFeedingTasks_Feedback title: BatchCreate9010VialFeedingTasks_Feedback
type: object
goal: goal:
properties: properties:
delay_time: delay_time:
default: '600'
description: 延迟时间(秒),默认600 description: 延迟时间(秒),默认600
type: string type: string
hold_m_name: hold_m_name:
@@ -189,11 +189,9 @@ bioyond_dispensing_station:
description: 10%物料的液体物料名称,默认为"NMP" description: 10%物料的液体物料名称,默认为"NMP"
type: string type: string
speed: speed:
default: '400'
description: 搅拌速度,默认400 description: 搅拌速度,默认400
type: string type: string
temperature: temperature:
default: '40'
description: 温度(℃),默认40 description: 温度(℃),默认40
type: string type: string
titration: titration:
@@ -202,21 +200,14 @@ bioyond_dispensing_station:
type: string type: string
required: required:
- titration - titration
- hold_m_name
title: BatchCreate9010VialFeedingTasks_Goal title: BatchCreate9010VialFeedingTasks_Goal
type: object type: object
result: result:
properties:
return_info:
description: 批量任务创建结果汇总JSON字符串包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息)
type: string
required:
- return_info
title: BatchCreate9010VialFeedingTasks_Result title: BatchCreate9010VialFeedingTasks_Result
type: object type: string
required: required:
- goal - goal
title: BatchCreate9010VialFeedingTasks title: batch_create_90_10_vial_feeding_tasks参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
batch_create_diamine_solution_tasks: batch_create_diamine_solution_tasks:
@@ -228,11 +219,11 @@ bioyond_dispensing_station:
speed: speed speed: speed
temperature: temperature temperature: temperature
goal_default: goal_default:
delay_time: '600' delay_time: null
liquid_material_name: NMP liquid_material_name: NMP
solutions: '' solutions: null
speed: '400' speed: null
temperature: '20' temperature: null
handles: handles:
input: input:
- data_key: solutions - data_key: solutions
@@ -248,20 +239,16 @@ bioyond_dispensing_station:
handler_key: BATCH_CREATE_RESULT handler_key: BATCH_CREATE_RESULT
io_type: sink io_type: sink
label: Complete Batch Create Result JSON (contains order_codes and order_ids) label: Complete Batch Create Result JSON (contains order_codes and order_ids)
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。 description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。
properties: properties:
feedback: feedback:
properties: {}
required: []
title: BatchCreateDiamineSolutionTasks_Feedback title: BatchCreateDiamineSolutionTasks_Feedback
type: object
goal: goal:
properties: properties:
delay_time: delay_time:
default: '600'
description: 溶液配置完成后的延迟时间用于充分混合和溶解默认600秒 description: 溶液配置完成后的延迟时间用于充分混合和溶解默认600秒
type: string type: string
liquid_material_name: liquid_material_name:
@@ -275,11 +262,9 @@ bioyond_dispensing_station:
4.5, "solvent_volume": 18}]' 4.5, "solvent_volume": 18}]'
type: string type: string
speed: speed:
default: '400'
description: 搅拌速度rpm用于混合溶液默认400转/分钟 description: 搅拌速度rpm用于混合溶液默认400转/分钟
type: string type: string
temperature: temperature:
default: '20'
description: 配置温度溶液配置过程的目标温度默认20℃室温 description: 配置温度溶液配置过程的目标温度默认20℃室温
type: string type: string
required: required:
@@ -287,17 +272,11 @@ bioyond_dispensing_station:
title: BatchCreateDiamineSolutionTasks_Goal title: BatchCreateDiamineSolutionTasks_Goal
type: object type: object
result: result:
properties:
return_info:
description: 批量任务创建结果汇总JSON字符串包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息)
type: string
required:
- return_info
title: BatchCreateDiamineSolutionTasks_Result title: BatchCreateDiamineSolutionTasks_Result
type: object type: string
required: required:
- goal - goal
title: BatchCreateDiamineSolutionTasks title: batch_create_diamine_solution_tasks参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
compute_experiment_design: compute_experiment_design:
@@ -309,7 +288,7 @@ bioyond_dispensing_station:
wt_percent: wt_percent wt_percent: wt_percent
goal_default: goal_default:
m_tot: '70' m_tot: '70'
ratio: '' ratio: null
titration_percent: '0.03' titration_percent: '0.03'
wt_percent: '0.25' wt_percent: '0.25'
handles: handles:
@@ -338,12 +317,8 @@ bioyond_dispensing_station:
handler_key: feeding_order handler_key: feeding_order
io_type: sink io_type: sink
label: Feeding Order Data From Calculation Node label: Feeding Order Data From Calculation Node
result: placeholder_keys: {}
feeding_order: feeding_order result: {}
return_info: return_info
solutions: solutions
solvents: solvents
titration: titration
schema: schema:
description: 计算实验设计输出solutions/titration/solvents/feeding_order用于后续节点。 description: 计算实验设计输出solutions/titration/solvents/feeding_order用于后续节点。
properties: properties:
@@ -356,7 +331,7 @@ bioyond_dispensing_station:
type: string type: string
ratio: ratio:
description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1} description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1}
type: string type: object
titration_percent: titration_percent:
default: '0.03' default: '0.03'
description: 滴定比例(10%部分) description: 滴定比例(10%部分)
@@ -371,14 +346,23 @@ bioyond_dispensing_station:
result: result:
properties: properties:
feeding_order: feeding_order:
items: {}
title: Feeding Order
type: array type: array
return_info: return_info:
title: Return Info
type: string type: string
solutions: solutions:
items: {}
title: Solutions
type: array type: array
solvents: solvents:
additionalProperties: true
title: Solvents
type: object type: object
titration: titration:
additionalProperties: true
title: Titration
type: object type: object
required: required:
- solutions - solutions
@@ -386,11 +370,11 @@ bioyond_dispensing_station:
- solvents - solvents
- feeding_order - feeding_order
- return_info - return_info
title: ComputeExperimentDesign_Result title: ComputeExperimentDesignReturn
type: object type: object
required: required:
- goal - goal
title: ComputeExperimentDesign title: compute_experiment_design参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
create_90_10_vial_feeding_task: create_90_10_vial_feeding_task:
@@ -444,17 +428,18 @@ bioyond_dispensing_station:
speed: '' speed: ''
temperature: '' temperature: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: DispenStationVialFeed_Feedback title: DispenStationVialFeed_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
delay_time: delay_time:
type: string type: string
@@ -502,38 +487,13 @@ bioyond_dispensing_station:
type: string type: string
temperature: temperature:
type: string type: string
required:
- order_name
- percent_90_1_assign_material_name
- percent_90_1_target_weigh
- percent_90_2_assign_material_name
- percent_90_2_target_weigh
- percent_90_3_assign_material_name
- percent_90_3_target_weigh
- percent_10_1_assign_material_name
- percent_10_1_target_weigh
- percent_10_1_volume
- percent_10_1_liquid_material_name
- percent_10_2_assign_material_name
- percent_10_2_target_weigh
- percent_10_2_volume
- percent_10_2_liquid_material_name
- percent_10_3_assign_material_name
- percent_10_3_target_weigh
- percent_10_3_volume
- percent_10_3_liquid_material_name
- speed
- temperature
- delay_time
- hold_m_name
title: DispenStationVialFeed_Goal title: DispenStationVialFeed_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: DispenStationVialFeed_Result title: DispenStationVialFeed_Result
type: object type: object
required: required:
@@ -564,17 +524,18 @@ bioyond_dispensing_station:
temperature: '' temperature: ''
volume: '' volume: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: DispenStationSolnPrep_Feedback title: DispenStationSolnPrep_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
delay_time: delay_time:
type: string type: string
@@ -594,24 +555,13 @@ bioyond_dispensing_station:
type: string type: string
volume: volume:
type: string type: string
required:
- order_name
- material_name
- target_weigh
- volume
- liquid_material_name
- speed
- temperature
- delay_time
- hold_m_name
title: DispenStationSolnPrep_Goal title: DispenStationSolnPrep_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: DispenStationSolnPrep_Result title: DispenStationSolnPrep_Result
type: object type: object
required: required:
@@ -624,8 +574,8 @@ bioyond_dispensing_station:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 启动调度器 - 启动Bioyond配液站的任务调度器开始执行队列中的任务 description: 启动调度器 - 启动Bioyond配液站的任务调度器开始执行队列中的任务
properties: properties:
@@ -635,12 +585,6 @@ bioyond_dispensing_station:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 调度器启动结果成功返回1失败返回0
type: integer
required:
- return_info
title: scheduler_start结果 title: scheduler_start结果
type: object type: object
required: required:
@@ -654,8 +598,8 @@ bioyond_dispensing_station:
target_device_id: target_device_id target_device_id: target_device_id
transfer_groups: transfer_groups transfer_groups: transfer_groups
goal_default: goal_default:
target_device_id: '' target_device_id: null
transfer_groups: '' transfer_groups: null
handles: {} handles: {}
placeholder_keys: placeholder_keys:
target_device_id: unilabos_devices target_device_id: unilabos_devices
@@ -671,32 +615,13 @@ bioyond_dispensing_station:
type: string type: string
transfer_groups: transfer_groups:
description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组 description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组
items:
properties:
materials:
description: 物料名称手动输入系统将通过RPC查询验证
type: string
target_sites:
description: 目标库位(手动输入,如"A01"
type: string
target_stack:
description: 目标堆栈名称(从列表选择)
enum:
- 堆栈1左
- 堆栈1右
- 站内试剂存放堆栈
type: string
required:
- materials
- target_stack
- target_sites
type: object
type: array type: array
required: required:
- target_device_id - target_device_id
- transfer_groups - transfer_groups
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: transfer_materials_to_reaction_station参数 title: transfer_materials_to_reaction_station参数
@@ -709,9 +634,9 @@ bioyond_dispensing_station:
check_interval: check_interval check_interval: check_interval
timeout: timeout timeout: timeout
goal_default: goal_default:
batch_create_result: '' batch_create_result: null
check_interval: '10' check_interval: 10
timeout: '7200' timeout: 7200
handles: handles:
input: input:
- data_key: batch_create_result - data_key: batch_create_result
@@ -727,47 +652,35 @@ bioyond_dispensing_station:
handler_key: batch_reports_result handler_key: batch_reports_result
io_type: sink io_type: sink
label: Batch Order Completion Reports label: Batch Order Completion Reports
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象并行监控所有任务状态并返回每个任务的报告。 description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象并行监控所有任务状态并返回每个任务的报告。
properties: properties:
feedback: feedback:
properties: {}
required: []
title: WaitForMultipleOrdersAndGetReports_Feedback title: WaitForMultipleOrdersAndGetReports_Feedback
type: object
goal: goal:
properties: properties:
batch_create_result: batch_create_result:
description: 批量创建任务的返回结果对象包含order_codes和order_ids数组。从上游batch_create节点通过handle传递 description: 批量创建任务的返回结果对象包含order_codes和order_ids数组。从上游batch_create节点通过handle传递
type: string type: string
check_interval: check_interval:
default: '10' default: 10
description: 检查任务状态的时间间隔默认每10秒检查一次所有待完成任务 description: 检查任务状态的时间间隔默认每10秒检查一次所有待完成任务
type: string type: integer
timeout: timeout:
default: '7200' default: 7200
description: 等待超时时间默认7200秒2小时。超过此时间未完成的任务将标记为timeout description: 等待超时时间默认7200秒2小时。超过此时间未完成的任务将标记为timeout
type: string type: integer
required: required: []
- batch_create_result
title: WaitForMultipleOrdersAndGetReports_Goal title: WaitForMultipleOrdersAndGetReports_Goal
type: object type: object
result: result:
properties:
return_info:
description: 'JSON格式的批量任务完成信息包含: total(总数), completed(成功数), timeout(超时数),
error(错误数), elapsed_time(总耗时), reports(报告数组每个元素包含order_code,
order_id, status, completion_status, report, elapsed_time)'
type: string
required:
- return_info
title: WaitForMultipleOrdersAndGetReports_Result title: WaitForMultipleOrdersAndGetReports_Result
type: object type: object
required: required:
- goal - goal
title: WaitForMultipleOrdersAndGetReports title: wait_for_multiple_orders_and_get_reports参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation

View File

@@ -1,78 +0,0 @@
camera:
category:
- camera
class:
action_value_mappings:
auto-destroy_node:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 用于安全地关闭摄像头设备释放摄像头资源停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: destroy_node参数
type: object
type: UniLabJsonCommand
auto-timer_callback:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧将OpenCV格式的图像转换为ROS Image消息格式并发布到指定的视频话题。默认以10Hz频率执行确保视频流的连续性和实时性。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: timer_callback参数
type: object
type: UniLabJsonCommand
module: unilabos.ros.nodes.presets.camera:VideoPublisher
status_types: {}
type: ros2
config_info: []
description: VideoPublisher摄像头设备节点用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头如USB摄像头、内置摄像头等定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。
handles: []
icon: ''
init_param_schema:
config:
properties:
camera_index:
default: 0
type: string
device_id:
default: video_publisher
type: string
device_uuid:
default: ''
type: string
period:
default: 0.1
type: number
resource_tracker:
type: object
required: []
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -18,7 +18,7 @@ cameracontroller_device:
goal: goal:
properties: properties:
config: config:
type: string type: object
required: [] required: []
type: object type: object
result: {} result: {}
@@ -42,7 +42,8 @@ cameracontroller_device:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: stop参数 title: stop参数
@@ -50,7 +51,7 @@ cameracontroller_device:
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.cameraSII.cameraUSB:CameraController module: unilabos.devices.cameraSII.cameraUSB:CameraController
status_types: status_types:
status: dict status: Dict[str, Any]
type: python type: python
config_info: [] config_info: []
description: Uni-Lab-OS 摄像头驱动Linux USB 摄像头版,无 PTZ description: Uni-Lab-OS 摄像头驱动Linux USB 摄像头版,无 PTZ
@@ -103,5 +104,4 @@ cameracontroller_device:
required: required:
- status - status
type: object type: object
registry_type: device
version: 1.0.0 version: 1.0.0

View File

@@ -141,30 +141,26 @@ hplc.agilent:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -175,7 +171,6 @@ hplc.agilent:
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
status_types: status_types:
could_run: bool could_run: bool
data_file: String
device_status: str device_status: str
driver_init_ok: bool driver_init_ok: bool
finish_status: str finish_status: str
@@ -199,10 +194,6 @@ hplc.agilent:
properties: properties:
could_run: could_run:
type: boolean type: boolean
data_file:
items:
type: string
type: array
device_status: device_status:
type: string type: string
driver_init_ok: driver_init_ok:
@@ -216,14 +207,13 @@ hplc.agilent:
success: success:
type: boolean type: boolean
required: required:
- status_text
- device_status
- could_run - could_run
- device_status
- driver_init_ok - driver_init_ok
- is_running
- success
- finish_status - finish_status
- data_file - is_running
- status_text
- success
type: object type: object
version: 1.0.0 version: 1.0.0
hplc.agilent-zhida: hplc.agilent-zhida:
@@ -236,26 +226,25 @@ hplc.agilent-zhida:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -315,21 +304,18 @@ hplc.agilent-zhida:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -341,35 +327,35 @@ hplc.agilent-zhida:
feedback: {} feedback: {}
goal: goal:
string: string string: string
text: text
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -407,7 +393,7 @@ hplc.agilent-zhida:
status: status:
type: object type: object
required: required:
- status
- methods - methods
- status
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -120,42 +120,41 @@ raman.home_made:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
raman_cmd: raman_cmd:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:

View File

@@ -19,7 +19,8 @@ separator.chinwe:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: connect参数 title: connect参数
@@ -65,135 +66,145 @@ separator.chinwe:
required: required:
- command_dict - command_dict
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: execute_command_from_outer参数 title: execute_command_from_outer参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
motor_rotate_quarter: motor_rotate_quarter:
feedback: {}
goal: goal:
direction: 顺时针 direction: 顺时针
motor_id: 4 motor_id: 4
speed: 60 speed: 60
goal_default:
direction: 顺时针
motor_id: null
speed: 60
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 电机旋转 1/4 圈 description: 电机旋转 1/4 圈
properties: properties:
feedback: {}
goal: goal:
properties: properties:
direction: direction:
default: 顺时针 default: 顺时针
description: 旋转方向 description: 旋转方向
enum:
- 顺时针
- 逆时针
type: string type: string
motor_id: motor_id:
default: '4'
description: 选择电机 (4:搅拌, 5:旋钮) description: 选择电机 (4:搅拌, 5:旋钮)
enum: type: integer
- '4'
- '5'
type: string
speed: speed:
default: 60 default: 60
description: 速度 (RPM) description: 速度 (RPM)
type: integer type: integer
required: required:
- motor_id - motor_id
- speed
type: object type: object
result: {}
required:
- goal
title: motor_rotate_quarter参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
motor_run_continuous: motor_run_continuous:
feedback: {}
goal: goal:
direction: 顺时针 direction: 顺时针
motor_id: 4 motor_id: 4
speed: 60 speed: 60
goal_default:
direction: 顺时针
motor_id: null
speed: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 电机一直旋转 (速度模式) description: 电机一直旋转 (速度模式)
properties: properties:
feedback: {}
goal: goal:
properties: properties:
direction: direction:
default: 顺时针 default: 顺时针
description: 旋转方向 description: 旋转方向
enum:
- 顺时针
- 逆时针
type: string type: string
motor_id: motor_id:
default: '4'
description: 选择电机 (4:搅拌, 5:旋钮) description: 选择电机 (4:搅拌, 5:旋钮)
enum: type: integer
- '4'
- '5'
type: string
speed: speed:
default: 60
description: 速度 (RPM) description: 速度 (RPM)
type: integer type: integer
required: required:
- motor_id - motor_id
- speed - speed
type: object type: object
result: {}
required:
- goal
title: motor_run_continuous参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
motor_stop: motor_stop:
feedback: {}
goal: goal:
motor_id: 4 motor_id: 4
goal_default:
motor_id: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 停止指定步进电机 description: 停止指定步进电机
properties: properties:
feedback: {}
goal: goal:
properties: properties:
motor_id: motor_id:
default: '4'
description: 选择电机 description: 选择电机
enum:
- '4'
- '5'
title: '注: 4=搅拌, 5=旋钮' title: '注: 4=搅拌, 5=旋钮'
type: string type: integer
required: required:
- motor_id - motor_id
type: object type: object
result: {}
required:
- goal
title: motor_stop参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pump_aspirate: pump_aspirate:
feedback: {}
goal: goal:
pump_id: 1 pump_id: 1
valve_port: 1 valve_port: 1
volume: 1000 volume: 1000
goal_default:
pump_id: null
valve_port: null
volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 注射泵吸液 description: 注射泵吸液
properties: properties:
feedback: {}
goal: goal:
properties: properties:
pump_id: pump_id:
default: '1'
description: 选择泵 description: 选择泵
enum: type: integer
- '1'
- '2'
- '3'
type: string
valve_port: valve_port:
default: '1'
description: 阀门端口 description: 阀门端口
enum: type: integer
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
volume: volume:
default: 1000
description: 吸液步数 description: 吸液步数
type: integer type: integer
required: required:
@@ -201,41 +212,38 @@ separator.chinwe:
- volume - volume
- valve_port - valve_port
type: object type: object
result: {}
required:
- goal
title: pump_aspirate参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pump_dispense: pump_dispense:
feedback: {}
goal: goal:
pump_id: 1 pump_id: 1
valve_port: 1 valve_port: 1
volume: 1000 volume: 1000
goal_default:
pump_id: null
valve_port: null
volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 注射泵排液 description: 注射泵排液
properties: properties:
feedback: {}
goal: goal:
properties: properties:
pump_id: pump_id:
default: '1'
description: 选择泵 description: 选择泵
enum: type: integer
- '1'
- '2'
- '3'
type: string
valve_port: valve_port:
default: '1'
description: 阀门端口 description: 阀门端口
enum: type: integer
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
volume: volume:
default: 1000
description: 排液步数 description: 排液步数
type: integer type: integer
required: required:
@@ -243,121 +251,152 @@ separator.chinwe:
- volume - volume
- valve_port - valve_port
type: object type: object
result: {}
required:
- goal
title: pump_dispense参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pump_initialize: pump_initialize:
feedback: {}
goal: goal:
drain_port: 0 drain_port: 0
output_port: 0 output_port: 0
pump_id: 1 pump_id: 1
speed: 10 speed: 10
goal_default:
drain_port: 0
output_port: 0
pump_id: null
speed: 10
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 初始化指定注射泵 description: 初始化指定注射泵
properties: properties:
feedback: {}
goal: goal:
properties: properties:
drain_port: drain_port:
default: 0 default: 0
description: 排液口索引 description: 排液口索引
type: integer type: string
output_port: output_port:
default: 0 default: 0
description: 输出口索引 description: 输出口索引
type: integer
pump_id:
default: '1'
description: 选择泵
enum:
- '1'
- '2'
- '3'
title: '注: 1号泵, 2号泵, 3号泵'
type: string type: string
pump_id:
description: 选择泵
title: '注: 1号泵, 2号泵, 3号泵'
type: integer
speed: speed:
default: 10 default: 10
description: 运动速度 description: 运动速度
type: integer type: string
required: required:
- pump_id - pump_id
type: object type: object
result: {}
required:
- goal
title: pump_initialize参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pump_valve: pump_valve:
feedback: {}
goal: goal:
port: 1 port: 1
pump_id: 1 pump_id: 1
goal_default:
port: null
pump_id: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 切换指定泵的阀门端口 description: 切换指定泵的阀门端口
properties: properties:
feedback: {}
goal: goal:
properties: properties:
port: port:
default: '1'
description: 阀门端口号 (1-8) description: 阀门端口号 (1-8)
enum: type: integer
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
pump_id: pump_id:
default: '1'
description: 选择泵 description: 选择泵
enum: type: integer
- '1'
- '2'
- '3'
type: string
required: required:
- pump_id - pump_id
- port - port
type: object type: object
result: {}
required:
- goal
title: pump_valve参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
wait_sensor_level: wait_sensor_level:
feedback: {}
goal: goal:
target_state: 有液 target_state: 有液
timeout: 30 timeout: 30
goal_default:
target_state: 有液
timeout: 30
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 等待传感器液位条件 description: 等待传感器液位条件
properties: properties:
feedback: {}
goal: goal:
properties: properties:
target_state: target_state:
default: 有液 default: 有液
description: 目标液位状态 description: 目标液位状态
enum:
- 有液
- 无液
type: string type: string
timeout: timeout:
default: 30 default: 30
description: 超时时间 (秒) description: 超时时间 (秒)
type: integer type: integer
required: required: []
- target_state
type: object type: object
result:
type: boolean
required:
- goal
title: wait_sensor_level参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
wait_time: wait_time:
feedback: {}
goal: goal:
duration: 10 duration: 10
goal_default:
duration: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 等待指定时间 description: 等待指定时间
properties: properties:
feedback: {}
goal: goal:
properties: properties:
duration: duration:
default: 10
description: 等待时间 (秒) description: 等待时间 (秒)
type: integer type: integer
required: required:
- duration - duration
type: object type: object
result:
type: boolean
required:
- goal
title: wait_time参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.separator.chinwe:ChinweDevice module: unilabos.devices.separator.chinwe:ChinweDevice
status_types: status_types:
@@ -406,8 +445,8 @@ separator.chinwe:
sensor_rssi: sensor_rssi:
type: integer type: integer
required: required:
- is_connected
- sensor_level - sensor_level
- sensor_rssi - sensor_rssi
- is_connected
type: object type: object
version: 2.1.0 version: 2.1.0

View File

@@ -64,7 +64,8 @@ coincellassemblyworkstation_device:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: fun_wuliao_test参数 title: fun_wuliao_test参数
@@ -109,7 +110,8 @@ coincellassemblyworkstation_device:
- elec_num - elec_num
- elec_use_num - elec_use_num
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: func_allpack_cmd参数 title: func_allpack_cmd参数
@@ -220,7 +222,8 @@ coincellassemblyworkstation_device:
- elec_num - elec_num
- elec_use_num - elec_use_num
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: func_allpack_cmd_simp参数 title: func_allpack_cmd_simp参数
@@ -309,7 +312,8 @@ coincellassemblyworkstation_device:
type: boolean type: boolean
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_device_init_auto_start_combined参数 title: func_pack_device_init_auto_start_combined参数
@@ -351,7 +355,8 @@ coincellassemblyworkstation_device:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_device_stop参数 title: func_pack_device_stop参数
@@ -376,7 +381,8 @@ coincellassemblyworkstation_device:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_get_msg_cmd参数 title: func_pack_get_msg_cmd参数
@@ -430,7 +436,8 @@ coincellassemblyworkstation_device:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_send_finished_cmd参数 title: func_pack_send_finished_cmd参数
@@ -467,7 +474,8 @@ coincellassemblyworkstation_device:
- assembly_type - assembly_type
- assembly_pressure - assembly_pressure
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_send_msg_cmd参数 title: func_pack_send_msg_cmd参数
@@ -611,7 +619,8 @@ coincellassemblyworkstation_device:
- elec_num - elec_num
- elec_use_num - elec_use_num
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: func_sendbottle_allpack_multi参数 title: func_sendbottle_allpack_multi参数
@@ -663,31 +672,6 @@ coincellassemblyworkstation_device:
title: modify_deck_name参数 title: modify_deck_name参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-qiming_coin_cell_code: auto-qiming_coin_cell_code:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -735,7 +719,8 @@ coincellassemblyworkstation_device:
required: required:
- fujipian_panshu - fujipian_panshu
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: qiming_coin_cell_code参数 title: qiming_coin_cell_code参数
@@ -826,25 +811,24 @@ coincellassemblyworkstation_device:
sys_status: sys_status:
type: string type: string
required: required:
- sys_status
- sys_mode
- request_rec_msg_status
- request_send_msg_status
- data_assembly_coin_cell_num - data_assembly_coin_cell_num
- data_assembly_pressure
- data_assembly_time - data_assembly_time
- data_open_circuit_voltage
- data_axis_x_pos - data_axis_x_pos
- data_axis_y_pos - data_axis_y_pos
- data_axis_z_pos - data_axis_z_pos
- data_pole_weight
- data_assembly_pressure
- data_electrolyte_volume
- data_coin_num
- data_coin_cell_code - data_coin_cell_code
- data_coin_num
- data_electrolyte_code - data_electrolyte_code
- data_glove_box_pressure - data_electrolyte_volume
- data_glove_box_o2_content - data_glove_box_o2_content
- data_glove_box_pressure
- data_glove_box_water_content - data_glove_box_water_content
- data_open_circuit_voltage
- data_pole_weight
- request_rec_msg_status
- request_send_msg_status
- sys_mode
- sys_status
type: object type: object
registry_type: device
version: 1.0.0 version: 1.0.0

View File

@@ -50,26 +50,25 @@ gas_source.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -82,26 +81,25 @@ gas_source.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -116,32 +114,31 @@ gas_source.mock:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -232,26 +229,25 @@ vacuum_pump.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -264,26 +260,25 @@ vacuum_pump.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -298,32 +293,31 @@ vacuum_pump.mock:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:

View File

@@ -5,7 +5,7 @@ hotel.thermo_orbitor_rs2_hotel:
action_value_mappings: {} action_value_mappings: {}
module: unilabos.devices.resource_container.container:HotelContainer module: unilabos.devices.resource_container.container:HotelContainer
status_types: status_types:
rotation: String rotation: ''
type: python type: python
config_info: [] config_info: []
description: Thermo Orbitor RS2 Hotel容器设备用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控主要用于存储实验样品、试剂瓶或其他实验器具支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。 description: Thermo Orbitor RS2 Hotel容器设备用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控主要用于存储实验样品、试剂瓶或其他实验器具支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。

View File

@@ -22,7 +22,8 @@ xyz_stepper_controller:
required: required:
- degrees - degrees
type: object type: object
result: {} result:
type: integer
required: required:
- goal - goal
title: degrees_to_steps参数 title: degrees_to_steps参数
@@ -47,7 +48,8 @@ xyz_stepper_controller:
required: required:
- axis - axis
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: emergency_stop参数 title: emergency_stop参数
@@ -72,7 +74,10 @@ xyz_stepper_controller:
type: boolean type: boolean
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: enable_all_axes参数 title: enable_all_axes参数
@@ -101,7 +106,8 @@ xyz_stepper_controller:
required: required:
- axis - axis
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: enable_motor参数 title: enable_motor参数
@@ -122,7 +128,10 @@ xyz_stepper_controller:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: home_all_axes参数 title: home_all_axes参数
@@ -147,7 +156,8 @@ xyz_stepper_controller:
required: required:
- axis - axis
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: home_axis参数 title: home_axis参数
@@ -188,7 +198,8 @@ xyz_stepper_controller:
- axis - axis
- position - position
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: move_to_position参数 title: move_to_position参数
@@ -229,7 +240,8 @@ xyz_stepper_controller:
- axis - axis
- degrees - degrees
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: move_to_position_degrees参数 title: move_to_position_degrees参数
@@ -270,7 +282,8 @@ xyz_stepper_controller:
- axis - axis
- revolutions - revolutions
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: move_to_position_revolutions参数 title: move_to_position_revolutions参数
@@ -301,14 +314,17 @@ xyz_stepper_controller:
default: 5000 default: 5000
type: integer type: integer
x: x:
type: string type: integer
y: y:
type: string type: integer
z: z:
type: string type: integer
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: move_xyz参数 title: move_xyz参数
@@ -339,14 +355,17 @@ xyz_stepper_controller:
default: 5000 default: 5000
type: integer type: integer
x_deg: x_deg:
type: string type: number
y_deg: y_deg:
type: string type: number
z_deg: z_deg:
type: string type: number
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: move_xyz_degrees参数 title: move_xyz_degrees参数
@@ -377,14 +396,17 @@ xyz_stepper_controller:
default: 5000 default: 5000
type: integer type: integer
x_rev: x_rev:
type: string type: number
y_rev: y_rev:
type: string type: number
z_rev: z_rev:
type: string type: number
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: move_xyz_revolutions参数 title: move_xyz_revolutions参数
@@ -409,7 +431,8 @@ xyz_stepper_controller:
required: required:
- revolutions - revolutions
type: object type: object
result: {} result:
type: integer
required: required:
- goal - goal
title: revolutions_to_steps参数 title: revolutions_to_steps参数
@@ -442,7 +465,8 @@ xyz_stepper_controller:
- axis - axis
- speed - speed
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: set_speed_mode参数 title: set_speed_mode参数
@@ -467,7 +491,8 @@ xyz_stepper_controller:
required: required:
- steps - steps
type: object type: object
result: {} result:
type: number
required: required:
- goal - goal
title: steps_to_degrees参数 title: steps_to_degrees参数
@@ -492,7 +517,8 @@ xyz_stepper_controller:
required: required:
- steps - steps
type: object type: object
result: {} result:
type: number
required: required:
- goal - goal
title: steps_to_revolutions参数 title: steps_to_revolutions参数
@@ -513,7 +539,10 @@ xyz_stepper_controller:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: stop_all_axes参数 title: stop_all_axes参数
@@ -542,7 +571,8 @@ xyz_stepper_controller:
required: required:
- axis - axis
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: wait_for_completion参数 title: wait_for_completion参数
@@ -550,8 +580,7 @@ xyz_stepper_controller:
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController
status_types: status_types:
all_positions: dict all_positions: Dict[MotorAxis, MotorPosition]
motor_status: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:MotorPosition
type: python type: python
config_info: [] config_info: []
description: 新XYZ控制器 description: 新XYZ控制器
@@ -574,12 +603,10 @@ xyz_stepper_controller:
data: data:
properties: properties:
all_positions: all_positions:
type: object additionalProperties:
motor_status: type: object
type: object type: object
required: required:
- motor_status
- all_positions - all_positions
type: object type: object
registry_type: device
version: 1.0.0 version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -5,31 +5,6 @@ neware_battery_test_system:
- battery_test - battery_test
class: class:
action_value_mappings: action_value_mappings:
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-print_status_summary: auto-print_status_summary:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -66,7 +41,8 @@ neware_battery_test_system:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: test_connection参数 title: test_connection参数
@@ -77,9 +53,8 @@ neware_battery_test_system:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
success: success
schema: schema:
description: 调试方法:显示所有资源的实际名称 description: 调试方法:显示所有资源的实际名称
properties: properties:
@@ -89,19 +64,10 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 资源调试信息
type: string
success:
description: 是否成功
type: boolean
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: debug_resource_names参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
export_status_json: export_status_json:
@@ -111,9 +77,8 @@ neware_battery_test_system:
goal_default: goal_default:
filepath: bts_status.json filepath: bts_status.json
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
success: success
schema: schema:
description: 导出当前状态数据到JSON文件 description: 导出当前状态数据到JSON文件
properties: properties:
@@ -127,19 +92,10 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 导出操作结果信息
type: string
success:
description: 导出是否成功
type: boolean
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: export_status_json参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
get_device_summary: get_device_summary:
@@ -181,10 +137,8 @@ neware_battery_test_system:
goal_default: goal_default:
plate_num: null plate_num: null
handles: {} handles: {}
result: placeholder_keys: {}
plate_data: plate_data result: {}
return_info: return_info
success: success
schema: schema:
description: 获取指定盘或所有盘的状态信息 description: 获取指定盘或所有盘的状态信息
properties: properties:
@@ -193,29 +147,14 @@ neware_battery_test_system:
properties: properties:
plate_num: plate_num:
description: 盘号 (1 或 2)如果为null则返回所有盘的状态 description: 盘号 (1 或 2)如果为null则返回所有盘的状态
maximum: 2
minimum: 1
type: integer type: integer
required: [] required: []
type: object type: object
result: result:
properties:
plate_data:
description: 盘状态数据(单盘或所有盘)
type: object
return_info:
description: 操作结果信息
type: string
success:
description: 查询是否成功
type: boolean
required:
- return_info
- success
- plate_data
type: object type: object
required: required:
- goal - goal
title: get_plate_status参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
print_status_summary_action: print_status_summary_action:
@@ -223,9 +162,8 @@ neware_battery_test_system:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
success: success
schema: schema:
description: 打印通道状态摘要信息到控制台 description: 打印通道状态摘要信息到控制台
properties: properties:
@@ -235,28 +173,21 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 打印操作结果信息
type: string
success:
description: 打印是否成功
type: boolean
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: print_status_summary_action参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
query_plate_action: query_plate_action:
feedback: {} feedback: {}
goal: goal:
string: plate_id plate_id: plate_id
string: string
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info return_info: return_info
success: success success: success
@@ -264,27 +195,23 @@ neware_battery_test_system:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -298,13 +225,11 @@ neware_battery_test_system:
csv_path: string csv_path: string
output_dir: string output_dir: string
goal_default: goal_default:
csv_path: '' csv_path: null
output_dir: . output_dir: .
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
submitted_count: submitted_count
success: success
schema: schema:
description: 从CSV文件批量提交Neware测试任务 description: 从CSV文件批量提交Neware测试任务
properties: properties:
@@ -315,31 +240,17 @@ neware_battery_test_system:
description: 输入CSV文件的绝对路径 description: 输入CSV文件的绝对路径
type: string type: string
output_dir: output_dir:
default: .
description: 输出目录用于存储XML和备份文件默认当前目录 description: 输出目录用于存储XML和备份文件默认当前目录
type: string type: string
required: required:
- csv_path - csv_path
type: object type: object
result: result:
properties:
return_info:
description: 执行结果详细信息
type: string
submitted_count:
description: 成功提交的任务数量
type: integer
success:
description: 是否成功
type: boolean
total_count:
description: CSV文件中的总行数
type: integer
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: submit_from_csv参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
test_connection_action: test_connection_action:
@@ -347,9 +258,8 @@ neware_battery_test_system:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
success: success
schema: schema:
description: 测试与电池测试系统的TCP连接 description: 测试与电池测试系统的TCP连接
properties: properties:
@@ -359,19 +269,10 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 连接测试结果信息
type: string
success:
description: 连接测试是否成功
type: boolean
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: test_connection_action参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
upload_backup_to_oss: upload_backup_to_oss:
@@ -392,12 +293,8 @@ neware_battery_test_system:
handler_key: uploaded_files handler_key: uploaded_files
io_type: sink io_type: sink
label: Uploaded Files (with standard flow info) label: Uploaded Files (with standard flow info)
result: placeholder_keys: {}
failed_files: failed_files result: {}
return_info: return_info
success: success
total_count: total_count
uploaded_count: uploaded_count
schema: schema:
description: 上传备份文件到阿里云OSS description: 上传备份文件到阿里云OSS
properties: properties:
@@ -417,65 +314,17 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
failed_files:
description: 上传失败的文件名列表
items:
type: string
type: array
return_info:
description: 上传操作结果信息
type: string
success:
description: 上传是否成功
type: boolean
total_count:
description: 总文件数
type: integer
uploaded_count:
description: 成功上传的文件数
type: integer
uploaded_files:
description: 成功上传的文件详情列表
items:
properties:
Battery_Code:
description: 电池编码
type: string
Electrolyte_Code:
description: 电解液编码
type: string
filename:
description: 文件名
type: string
url:
description: OSS下载链接
type: string
required:
- filename
- url
- Battery_Code
- Electrolyte_Code
type: object
type: array
required:
- return_info
- success
- uploaded_count
- total_count
- failed_files
- uploaded_files
type: object type: object
required: required:
- goal - goal
title: upload_backup_to_oss参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
status_types: status_types:
channel_status: dict channel_status: Dict[int, Dict]
connection_info: dict connection_info: Dict[str, str]
device_summary: dict device_summary: dict
plate_status: dict
status: str status: str
total_channels: int total_channels: int
type: python type: python
@@ -517,23 +366,24 @@ neware_battery_test_system:
data: data:
properties: properties:
channel_status: channel_status:
additionalProperties:
type: object
type: object type: object
connection_info: connection_info:
additionalProperties:
type: string
type: object type: object
device_summary: device_summary:
type: object type: object
plate_status:
type: object
status: status:
type: string type: string
total_channels: total_channels:
type: integer type: integer
required: required:
- status
- channel_status - channel_status
- connection_info - connection_info
- total_channels
- plate_status
- device_summary - device_summary
- status
- total_channels
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -142,8 +142,7 @@ opcua_example:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.device_comms.opcua_client.client:OpcUaClient module: unilabos.device_comms.opcua_client.client:OpcUaClient
status_types: status_types: {}
node_value: String
type: python type: python
config_info: [] config_info: []
description: null description: null
@@ -167,10 +166,7 @@ opcua_example:
- url - url
type: object type: object
data: data:
properties: properties: {}
node_value: required: []
type: string
required:
- node_value
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -80,7 +80,8 @@ opsky_ATR30007:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: run_once参数 title: run_once参数

View File

@@ -100,42 +100,41 @@ rotavap.one:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
set_timer: set_timer:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -250,9 +249,13 @@ separator.homemade:
feedback: feedback:
status: status status: status
goal: goal:
event: event
settling_time: settling_time settling_time: settling_time
stir_speed: stir_speed stir_speed: stir_speed
stir_time: stir_time, stir_time: stir_time
time: time
time_spec: time_spec
vessel: vessel
goal_default: goal_default:
event: '' event: ''
settling_time: '' settling_time: ''
@@ -281,34 +284,42 @@ separator.homemade:
sample_id: '' sample_id: ''
type: '' type: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
message: message
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: Stir_Feedback title: Stir_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
event: event:
type: string type: string
settling_time: settling_time:
type: string type: string
stir_speed: stir_speed:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
stir_time: stir_time:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
time: time:
type: string type: string
time_spec: time_spec:
type: string type: string
vessel: vessel:
additionalProperties: false
properties: properties:
category: category:
type: string type: string
@@ -327,16 +338,26 @@ separator.homemade:
parent: parent:
type: string type: string
pose: pose:
additionalProperties: false
properties: properties:
orientation: orientation:
additionalProperties: false
properties: properties:
w: w:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -346,12 +367,19 @@ separator.homemade:
title: orientation title: orientation
type: object type: object
position: position:
additionalProperties: false
properties: properties:
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -381,17 +409,10 @@ separator.homemade:
- data - data
title: vessel title: vessel
type: object type: object
required:
- vessel
- time
- event
- time_spec
- stir_time
- stir_speed
- settling_time
title: Stir_Goal title: Stir_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
message: message:
type: string type: string
@@ -399,10 +420,6 @@ separator.homemade:
type: string type: string
success: success:
type: boolean type: boolean
required:
- success
- message
- return_info
title: Stir_Result title: Stir_Result
type: object type: object
required: required:
@@ -418,36 +435,34 @@ separator.homemade:
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:

View File

@@ -28,31 +28,6 @@ post_process_station:
title: load_config参数 title: load_config参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-print_cache_stats: auto-print_cache_stats:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -104,42 +79,41 @@ post_process_station:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
disconnect: disconnect:
feedback: {} feedback:
status: status
goal: goal:
command: {} command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -149,42 +123,41 @@ post_process_station:
type: SendCmd type: SendCmd
read_node: read_node:
feedback: feedback:
result: result status: status
goal: goal:
command: node_name command: command
node_name: node_name
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -283,17 +256,19 @@ post_process_station:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: PostProcessTriggerClean_Feedback title: PostProcessTriggerClean_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
acetone_inner_wall_cleaning_count: acetone_inner_wall_cleaning_count:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
acetone_inner_wall_cleaning_injection: acetone_inner_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
acetone_inner_wall_cleaning_waste_time: acetone_inner_wall_cleaning_waste_time:
maximum: 2147483647 maximum: 2147483647
@@ -304,6 +279,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
acetone_outer_wall_cleaning_injection: acetone_outer_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
acetone_outer_wall_cleaning_wait_time: acetone_outer_wall_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -322,6 +299,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
acetone_stirrer_cleaning_injection: acetone_stirrer_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
acetone_stirrer_cleaning_wait_time: acetone_stirrer_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -348,6 +327,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
nmp_inner_wall_cleaning_injection: nmp_inner_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
nmp_inner_wall_cleaning_waste_time: nmp_inner_wall_cleaning_waste_time:
maximum: 2147483647 maximum: 2147483647
@@ -358,6 +339,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
nmp_outer_wall_cleaning_injection: nmp_outer_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
nmp_outer_wall_cleaning_wait_time: nmp_outer_wall_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -376,6 +359,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
nmp_stirrer_cleaning_injection: nmp_stirrer_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
nmp_stirrer_cleaning_wait_time: nmp_stirrer_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -394,6 +379,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
water_inner_wall_cleaning_injection: water_inner_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
water_inner_wall_cleaning_waste_time: water_inner_wall_cleaning_waste_time:
maximum: 2147483647 maximum: 2147483647
@@ -404,6 +391,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
water_outer_wall_cleaning_injection: water_outer_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
water_outer_wall_cleaning_wait_time: water_outer_wall_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -422,6 +411,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
water_stirrer_cleaning_injection: water_stirrer_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
water_stirrer_cleaning_wait_time: water_stirrer_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -431,55 +422,13 @@ post_process_station:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- nmp_outer_wall_cleaning_injection
- nmp_outer_wall_cleaning_count
- nmp_outer_wall_cleaning_wait_time
- nmp_outer_wall_cleaning_waste_time
- nmp_inner_wall_cleaning_injection
- nmp_inner_wall_cleaning_count
- nmp_pump_cleaning_suction_count
- nmp_inner_wall_cleaning_waste_time
- nmp_stirrer_cleaning_injection
- nmp_stirrer_cleaning_count
- nmp_stirrer_cleaning_wait_time
- nmp_stirrer_cleaning_waste_time
- water_outer_wall_cleaning_injection
- water_outer_wall_cleaning_count
- water_outer_wall_cleaning_wait_time
- water_outer_wall_cleaning_waste_time
- water_inner_wall_cleaning_injection
- water_inner_wall_cleaning_count
- water_pump_cleaning_suction_count
- water_inner_wall_cleaning_waste_time
- water_stirrer_cleaning_injection
- water_stirrer_cleaning_count
- water_stirrer_cleaning_wait_time
- water_stirrer_cleaning_waste_time
- acetone_outer_wall_cleaning_injection
- acetone_outer_wall_cleaning_count
- acetone_outer_wall_cleaning_wait_time
- acetone_outer_wall_cleaning_waste_time
- acetone_inner_wall_cleaning_injection
- acetone_inner_wall_cleaning_count
- acetone_pump_cleaning_suction_count
- acetone_inner_wall_cleaning_waste_time
- acetone_stirrer_cleaning_injection
- acetone_stirrer_cleaning_count
- acetone_stirrer_cleaning_wait_time
- acetone_stirrer_cleaning_waste_time
- pipe_blowing_time
- injection_pump_forward_empty_suction_count
- injection_pump_reverse_empty_suction_count
- filtration_liquid_selection
title: PostProcessTriggerClean_Goal title: PostProcessTriggerClean_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: PostProcessTriggerClean_Result title: PostProcessTriggerClean_Result
type: object type: object
required: required:
@@ -502,11 +451,11 @@ post_process_station:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: PostProcessGrab_Feedback title: PostProcessGrab_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
raw_tank_number: raw_tank_number:
maximum: 2147483647 maximum: 2147483647
@@ -516,17 +465,13 @@ post_process_station:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- reaction_tank_number
- raw_tank_number
title: PostProcessGrab_Goal title: PostProcessGrab_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: PostProcessGrab_Result title: PostProcessGrab_Result
type: object type: object
required: required:
@@ -573,13 +518,15 @@ post_process_station:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: PostProcessTriggerPostPro_Feedback title: PostProcessTriggerPostPro_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
atomization_fast_speed: atomization_fast_speed:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
atomization_pressure_kpa: atomization_pressure_kpa:
maximum: 2147483647 maximum: 2147483647
@@ -594,8 +541,12 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
first_wash_water_amount: first_wash_water_amount:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
initial_water_amount: initial_water_amount:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
injection_pump_push_speed: injection_pump_push_speed:
maximum: 2147483647 maximum: 2147483647
@@ -622,32 +573,20 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
second_wash_water_amount: second_wash_water_amount:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
wash_slow_speed: wash_slow_speed:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- atomization_fast_speed
- wash_slow_speed
- injection_pump_suction_speed
- injection_pump_push_speed
- raw_liquid_suction_count
- first_wash_water_amount
- second_wash_water_amount
- first_powder_mixing_tim
- second_powder_mixing_time
- first_powder_wash_count
- second_powder_wash_count
- initial_water_amount
- pre_filtration_mixing_time
- atomization_pressure_kpa
title: PostProcessTriggerPostPro_Goal title: PostProcessTriggerPostPro_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: PostProcessTriggerPostPro_Result title: PostProcessTriggerPostPro_Result
type: object type: object
required: required:
@@ -669,30 +608,26 @@ post_process_station:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -702,8 +637,7 @@ post_process_station:
type: SendCmd type: SendCmd
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
status_types: status_types:
cache_stats: dict cache_stats: Dict[str, Any]
node_value: String
type: python type: python
config_info: [] config_info: []
description: 后处理站 description: 后处理站
@@ -718,7 +652,9 @@ post_process_station:
config_path: config_path:
type: string type: string
deck: deck:
type: string anyOf:
- type: object
- type: object
password: password:
type: string type: string
subscription_interval: subscription_interval:
@@ -738,10 +674,7 @@ post_process_station:
properties: properties:
cache_stats: cache_stats:
type: object type: object
node_value:
type: string
required: required:
- node_value
- cache_stats - cache_stats
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -136,36 +136,36 @@ solenoid_valve:
set_valve_position: set_valve_position:
feedback: {} feedback: {}
goal: goal:
string: position position: position
string: string
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -278,26 +278,25 @@ solenoid_valve.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -310,26 +309,25 @@ solenoid_valve.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -422,6 +420,27 @@ syringe_pump_with_valve.runze.SY03B-T06:
title: initialize参数 title: initialize参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-list:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: list的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: list参数
type: object
type: UniLabJsonCommand
auto-pull_plunger: auto-pull_plunger:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -695,7 +714,10 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: goal:
properties: properties:
position: position:
type: string anyOf:
- type: integer
- type: string
- type: number
required: required:
- position - position
type: object type: object
@@ -720,7 +742,9 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: goal:
properties: properties:
velocity: velocity:
type: string anyOf:
- type: integer
- type: string
required: required:
- velocity - velocity
type: object type: object
@@ -780,13 +804,13 @@ syringe_pump_with_valve.runze.SY03B-T06:
status_types: status_types:
max_velocity: float max_velocity: float
mode: int mode: int
plunger_position: String plunger_position: ''
position: float position: float
status: str status: str
valve_position: str valve_position: str
velocity_end: String velocity_end: ''
velocity_grade: String velocity_grade: ''
velocity_init: String velocity_init: ''
type: python type: python
config_info: [] config_info: []
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
@@ -885,15 +909,15 @@ syringe_pump_with_valve.runze.SY03B-T06:
velocity_init: velocity_init:
type: string type: string
required: required:
- status
- mode
- max_velocity - max_velocity
- mode
- plunger_position
- position
- status
- valve_position
- velocity_end
- velocity_grade - velocity_grade
- velocity_init - velocity_init
- velocity_end
- valve_position
- position
- plunger_position
type: object type: object
version: 1.0.0 version: 1.0.0
syringe_pump_with_valve.runze.SY03B-T08: syringe_pump_with_valve.runze.SY03B-T08:
@@ -943,6 +967,27 @@ syringe_pump_with_valve.runze.SY03B-T08:
title: initialize参数 title: initialize参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-list:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: list的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: list参数
type: object
type: UniLabJsonCommand
auto-pull_plunger: auto-pull_plunger:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -1216,7 +1261,10 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: goal:
properties: properties:
position: position:
type: string anyOf:
- type: integer
- type: string
- type: number
required: required:
- position - position
type: object type: object
@@ -1241,7 +1289,9 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: goal:
properties: properties:
velocity: velocity:
type: string anyOf:
- type: integer
- type: string
required: required:
- velocity - velocity
type: object type: object
@@ -1301,13 +1351,13 @@ syringe_pump_with_valve.runze.SY03B-T08:
status_types: status_types:
max_velocity: float max_velocity: float
mode: int mode: int
plunger_position: String plunger_position: ''
position: float position: float
status: str status: str
valve_position: str valve_position: str
velocity_end: String velocity_end: ''
velocity_grade: String velocity_grade: ''
velocity_init: String velocity_init: ''
type: python type: python
config_info: [] config_info: []
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
@@ -1422,14 +1472,14 @@ syringe_pump_with_valve.runze.SY03B-T08:
velocity_init: velocity_init:
type: string type: string
required: required:
- status
- mode
- max_velocity - max_velocity
- mode
- plunger_position
- position
- status
- valve_position
- velocity_end
- velocity_grade - velocity_grade
- velocity_init - velocity_init
- velocity_end
- valve_position
- position
- plunger_position
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -13,12 +13,13 @@ reaction_station.bioyond:
start_point: start_point start_point: start_point
start_step_key: start_step_key start_step_key: start_step_key
goal_default: goal_default:
duration: 0 duration: null
end_point: 0 end_point: 0
end_step_key: '' end_step_key: ''
start_point: 0 start_point: 0
start_step_key: '' start_step_key: ''
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 添加时间约束 - 在两个工作流之间添加时间约束 description: 添加时间约束 - 在两个工作流之间添加时间约束
@@ -30,23 +31,19 @@ reaction_station.bioyond:
description: 时间(秒) description: 时间(秒)
type: integer type: integer
end_point: end_point:
default: Start default: 0
description: 终点计时点 (Start=开始前, End=结束后) description: 终点计时点 (Start=开始前, End=结束后)
enum: type: integer
- Start
- End
type: string
end_step_key: end_step_key:
default: ''
description: 终点步骤Key (可选, 默认为空则自动选择) description: 终点步骤Key (可选, 默认为空则自动选择)
type: string type: string
start_point: start_point:
default: Start default: 0
description: 起点计时点 (Start=开始前, End=结束后) description: 起点计时点 (Start=开始前, End=结束后)
enum: type: integer
- Start
- End
type: string
start_step_key: start_step_key:
default: ''
description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择) description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择)
type: string type: string
required: required:
@@ -98,7 +95,8 @@ reaction_station.bioyond:
required: required:
- json_str - json_str
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_order参数 title: create_order参数
@@ -125,7 +123,8 @@ reaction_station.bioyond:
required: required:
- workflow_ids - workflow_ids
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: hard_delete_merged_workflows参数 title: hard_delete_merged_workflows参数
@@ -150,7 +149,8 @@ reaction_station.bioyond:
required: required:
- json_str - json_str
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: merge_workflow_with_parameters参数 title: merge_workflow_with_parameters参数
@@ -175,7 +175,8 @@ reaction_station.bioyond:
required: required:
- report_request - report_request
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: process_temperature_cutoff_report参数 title: process_temperature_cutoff_report参数
@@ -200,7 +201,12 @@ reaction_station.bioyond:
required: required:
- web_workflow_json - web_workflow_json
type: object type: object
result: {} result:
items:
additionalProperties:
type: string
type: object
type: array
required: required:
- goal - goal
title: process_web_workflows参数 title: process_web_workflows参数
@@ -229,7 +235,8 @@ reaction_station.bioyond:
- reactor_id - reactor_id
- temperature - temperature
type: object type: object
result: {} result:
type: string
required: required:
- goal - goal
title: set_reactor_temperature参数 title: set_reactor_temperature参数
@@ -254,7 +261,8 @@ reaction_station.bioyond:
required: required:
- preintake_id - preintake_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: skip_titration_steps参数 title: skip_titration_steps参数
@@ -275,7 +283,8 @@ reaction_station.bioyond:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: sync_workflow_sequence_from_bioyond参数 title: sync_workflow_sequence_from_bioyond参数
@@ -307,7 +316,8 @@ reaction_station.bioyond:
type: integer type: integer
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: wait_for_multiple_orders_and_get_reports参数 title: wait_for_multiple_orders_and_get_reports参数
@@ -359,7 +369,8 @@ reaction_station.bioyond:
required: required:
- workflow_id - workflow_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: workflow_step_query参数 title: workflow_step_query参数
@@ -370,9 +381,8 @@ reaction_station.bioyond:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
code: code result: {}
message: message
schema: schema:
description: 清空服务端所有非核心工作流 (保留核心流程) description: 清空服务端所有非核心工作流 (保留核心流程)
properties: properties:
@@ -382,13 +392,6 @@ reaction_station.bioyond:
required: [] required: []
type: object type: object
result: result:
properties:
code:
description: 操作结果代码(1表示成功)
type: integer
message:
description: 结果描述
type: string
type: object type: object
required: required:
- goal - goal
@@ -405,13 +408,14 @@ reaction_station.bioyond:
torque_variation: torque_variation torque_variation: torque_variation
volume: volume volume: volume
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
temperature: '' temperature: 25.0
time: '' time: '90'
titration_type: '' titration_type: '1'
torque_variation: '' torque_variation: 2
volume: '' volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 滴回去 description: 滴回去
@@ -423,33 +427,27 @@ reaction_station.bioyond:
description: 物料名称(不能为空) description: 物料名称(不能为空)
type: string type: string
temperature: temperature:
default: 25.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
time: time:
default: '90'
description: 观察时间(分钟) description: 观察时间(分钟)
type: string type: string
titration_type: titration_type:
default: '1'
description: 是否滴定(NO=否, YES=是) description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 2
description: 是否观察 (NO=否, YES=是) description: 是否观察 (NO=否, YES=是)
enum: type: integer
- 'NO'
- 'YES'
type: string
volume: volume:
description: 分液公式(mL) description: 分液公式(mL)
type: string type: string
required: required:
- volume
- assign_material_name - assign_material_name
- time - volume
- torque_variation
- titration_type
- temperature
type: object type: object
result: {} result: {}
required: required:
@@ -462,7 +460,7 @@ reaction_station.bioyond:
goal: goal:
batch_reports_result: batch_reports_result batch_reports_result: batch_reports_result
goal_default: goal_default:
batch_reports_result: '' batch_reports_result: null
handles: handles:
input: input:
- data_key: batch_reports_result - data_key: batch_reports_result
@@ -478,8 +476,8 @@ reaction_station.bioyond:
handler_key: ACTUALS_EXTRACTED handler_key: ACTUALS_EXTRACTED
io_type: sink io_type: sink
label: Extracted Actuals label: Extracted Actuals
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 从批量任务完成报告中提取每个订单的实际加料量输出extracted列表。 description: 从批量任务完成报告中提取每个订单的实际加料量输出extracted列表。
properties: properties:
@@ -493,13 +491,6 @@ reaction_station.bioyond:
- batch_reports_result - batch_reports_result
type: object type: object
result: result:
properties:
return_info:
description: JSON字符串包含actuals数组每项含order_code, order_id, actualTargetWeigh,
actualVolume
type: string
required:
- return_info
title: extract_actuals_from_batch_reports结果 title: extract_actuals_from_batch_reports结果
type: object type: object
required: required:
@@ -517,13 +508,14 @@ reaction_station.bioyond:
torque_variation: torque_variation torque_variation: torque_variation
volume: volume volume: volume
goal_default: goal_default:
assign_material_name: '' assign_material_name: BAPP
temperature: '' temperature: 25.0
time: '' time: '0'
titration_type: '' titration_type: '1'
torque_variation: '' torque_variation: 1
volume: '' volume: '350'
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 液体进料烧杯 description: 液体进料烧杯
@@ -532,36 +524,30 @@ reaction_station.bioyond:
goal: goal:
properties: properties:
assign_material_name: assign_material_name:
default: BAPP
description: 物料名称 description: 物料名称
type: string type: string
temperature: temperature:
default: 25.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
time: time:
default: '0'
description: 观察时间(分钟) description: 观察时间(分钟)
type: string type: string
titration_type: titration_type:
default: '1'
description: 是否滴定(NO=否, YES=是) description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 1
description: 是否观察 (NO=否, YES=是) description: 是否观察 (NO=否, YES=是)
enum: type: integer
- 'NO'
- 'YES'
type: string
volume: volume:
default: '350'
description: 分液公式(mL) description: 分液公式(mL)
type: string type: string
required: required: []
- volume
- assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object type: object
result: {} result: {}
required: required:
@@ -580,13 +566,13 @@ reaction_station.bioyond:
torque_variation: torque_variation torque_variation: torque_variation
volume: volume volume: volume
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
solvents: '' solvents: null
temperature: '25.00' temperature: 25.0
time: '360' time: '360'
titration_type: '1' titration_type: '1'
torque_variation: '2' torque_variation: 2
volume: '' volume: null
handles: handles:
input: input:
- data_key: solvents - data_key: solvents
@@ -595,6 +581,7 @@ reaction_station.bioyond:
handler_key: solvents handler_key: solvents
io_type: source io_type: source
label: Solvents Data From Calculation Node label: Solvents Data From Calculation Node
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。 description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。
@@ -609,27 +596,21 @@ reaction_station.bioyond:
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume' description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
type: string type: string
temperature: temperature:
default: '25.00' default: 25.0
description: 温度设定(°C),默认25.00 description: 温度设定(°C),默认25.00
type: string type: number
time: time:
default: '360' default: '360'
description: 观察时间(分钟),默认360 description: 观察时间(分钟),默认360
type: string type: string
titration_type: titration_type:
default: 'NO' default: '1'
description: 是否滴定(NO=否, YES=是),默认NO description: 是否滴定(NO=否, YES=是),默认NO
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 'YES' default: 2
description: 是否观察 (NO=否, YES=是),默认YES description: 是否观察 (NO=否, YES=是),默认YES
enum: type: integer
- 'NO'
- 'YES'
type: string
volume: volume:
description: 分液量(mL)。可直接提供,或通过solvents参数自动计算 description: 分液量(mL)。可直接提供,或通过solvents参数自动计算
type: string type: string
@@ -655,15 +636,15 @@ reaction_station.bioyond:
volume_formula: volume_formula volume_formula: volume_formula
x_value: x_value x_value: x_value
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
extracted_actuals: '' extracted_actuals: null
feeding_order_data: '' feeding_order_data: null
temperature: '25.00' temperature: 25.0
time: '90' time: '90'
titration_type: '2' titration_type: '2'
torque_variation: '2' torque_variation: 2
volume_formula: '' volume_formula: null
x_value: '' x_value: null
handles: handles:
input: input:
- data_key: extracted_actuals - data_key: extracted_actuals
@@ -678,6 +659,7 @@ reaction_station.bioyond:
handler_key: feeding_order handler_key: feeding_order
io_type: source io_type: source
label: Feeding Order Data From Calculation Node label: Feeding Order Data From Calculation Node
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定" description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定"
@@ -696,27 +678,21 @@ reaction_station.bioyond:
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}' {"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
type: string type: string
temperature: temperature:
default: '25.00' default: 25.0
description: 温度设定(°C),默认25.00 description: 温度设定(°C),默认25.00
type: string type: number
time: time:
default: '90' default: '90'
description: 观察时间(分钟),默认90 description: 观察时间(分钟),默认90
type: string type: string
titration_type: titration_type:
default: 'YES' default: '2'
description: 是否滴定(NO=否, YES=是),默认YES description: 是否滴定(NO=否, YES=是),默认YES
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 'YES' default: 2
description: 是否观察 (NO=否, YES=是),默认YES description: 是否观察 (NO=否, YES=是),默认YES
enum: type: integer
- 'NO'
- 'YES'
type: string
volume_formula: volume_formula:
description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成 description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
type: string type: string
@@ -742,13 +718,14 @@ reaction_station.bioyond:
torque_variation: torque_variation torque_variation: torque_variation
volume_formula: volume_formula volume_formula: volume_formula
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
temperature: '' temperature: 25.0
time: '' time: '0'
titration_type: '' titration_type: '1'
torque_variation: '' torque_variation: 1
volume_formula: '' volume_formula: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 液体进料小瓶(非滴定) description: 液体进料小瓶(非滴定)
@@ -760,33 +737,27 @@ reaction_station.bioyond:
description: 物料名称 description: 物料名称
type: string type: string
temperature: temperature:
default: 25.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
time: time:
default: '0'
description: 观察时间(分钟) description: 观察时间(分钟)
type: string type: string
titration_type: titration_type:
default: '1'
description: 是否滴定(NO=否, YES=是) description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 1
description: 是否观察 (NO=否, YES=是) description: 是否观察 (NO=否, YES=是)
enum: type: integer
- 'NO'
- 'YES'
type: string
volume_formula: volume_formula:
description: 分液公式(mL) description: 分液公式(mL)
type: string type: string
required: required:
- volume_formula - volume_formula
- assign_material_name - assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object type: object
result: {} result: {}
required: required:
@@ -800,9 +771,10 @@ reaction_station.bioyond:
task_name: task_name task_name: task_name
workflow_name: workflow_name workflow_name: workflow_name
goal_default: goal_default:
task_name: '' task_name: null
workflow_name: '' workflow_name: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 处理并执行工作流 description: 处理并执行工作流
@@ -820,7 +792,8 @@ reaction_station.bioyond:
- workflow_name - workflow_name
- task_name - task_name
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: process_and_execute_workflow参数 title: process_and_execute_workflow参数
@@ -833,10 +806,11 @@ reaction_station.bioyond:
cutoff: cutoff cutoff: cutoff
temperature: temperature temperature: temperature
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
cutoff: '' cutoff: '900000'
temperature: '' temperature: -10.0
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数 description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数
@@ -848,14 +822,14 @@ reaction_station.bioyond:
description: 物料名称 description: 物料名称
type: string type: string
cutoff: cutoff:
default: '900000'
description: 粘度上限 description: 粘度上限
type: string type: string
temperature: temperature:
default: -10.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
required: required:
- cutoff
- temperature
- assign_material_name - assign_material_name
type: object type: object
result: {} result: {}
@@ -869,6 +843,7 @@ reaction_station.bioyond:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作 description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作
@@ -878,15 +853,7 @@ reaction_station.bioyond:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: result: {}
properties:
code:
description: 操作结果代码(1表示成功,0表示失败)
type: integer
return_info:
description: 操作结果详细信息
type: string
type: object
required: required:
- goal - goal
title: reactor_taken_out参数 title: reactor_taken_out参数
@@ -897,8 +864,8 @@ reaction_station.bioyond:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 启动调度器 - 启动Bioyond工作站的任务调度器开始执行队列中的任务 description: 启动调度器 - 启动Bioyond工作站的任务调度器开始执行队列中的任务
properties: properties:
@@ -908,12 +875,6 @@ reaction_station.bioyond:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 调度器启动结果成功返回1失败返回0
type: integer
required:
- return_info
title: scheduler_start结果 title: scheduler_start结果
type: object type: object
required: required:
@@ -930,12 +891,13 @@ reaction_station.bioyond:
time: time time: time
torque_variation: torque_variation torque_variation: torque_variation
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
material_id: '' material_id: null
temperature: '' temperature: 25.0
time: '' time: '0'
torque_variation: '' torque_variation: 1
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA) description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA)
@@ -948,29 +910,21 @@ reaction_station.bioyond:
type: string type: string
material_id: material_id:
description: 粉末类型IDSalt=盐21分钟Flour=面粉27分钟BTDA=BTDA38分钟 description: 粉末类型IDSalt=盐21分钟Flour=面粉27分钟BTDA=BTDA38分钟
enum:
- Salt
- Flour
- BTDA
type: string type: string
temperature: temperature:
default: 25.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
time: time:
default: '0'
description: 观察时间(分钟) description: 观察时间(分钟)
type: string type: string
torque_variation: torque_variation:
default: 1
description: 是否观察 (NO=否, YES=是) description: 是否观察 (NO=否, YES=是)
enum: type: integer
- 'NO'
- 'YES'
type: string
required: required:
- assign_material_name
- material_id - material_id
- time
- torque_variation
- temperature
type: object type: object
result: {} result: {}
required: required:

View File

@@ -37,42 +37,41 @@ agv.SEER:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
send_nav_task: send_nav_task:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:

View File

@@ -122,31 +122,6 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
title: moveit_task参数 title: moveit_task参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: post_init的参数schema
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-resource_manager: auto-resource_manager:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -198,41 +173,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pick_and_place: pick_and_place:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -241,41 +216,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
type: object type: object
type: SendCmd type: SendCmd
set_position: set_position:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -284,41 +259,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
type: object type: object
type: SendCmd type: SendCmd
set_status: set_status:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -455,42 +430,41 @@ robotic_arm.UR:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
move_pos_task: move_pos_task:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -532,8 +506,8 @@ robotic_arm.UR:
type: string type: string
required: required:
- arm_pose - arm_pose
- gripper_pose
- arm_status - arm_status
- gripper_pose
- gripper_status - gripper_status
type: object type: object
version: 1.0.0 version: 1.0.0
@@ -726,41 +700,41 @@ robotic_arm.elite:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
modbus_task_cmd: modbus_task_cmd:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -770,8 +744,8 @@ robotic_arm.elite:
type: SendCmd type: SendCmd
module: unilabos.devices.arm.elite_robot:EliteRobot module: unilabos.devices.arm.elite_robot:EliteRobot
status_types: status_types:
actual_joint_positions: String actual_joint_positions: ''
arm_pose: String arm_pose: list[float]
type: python type: python
config_info: [] config_info: []
description: Elite robot arm description: Elite robot arm
@@ -797,8 +771,8 @@ robotic_arm.elite:
type: number type: number
type: array type: array
required: required:
- arm_pose
- actual_joint_positions - actual_joint_positions
- arm_pose
type: object type: object
model: model:
mesh: elite_robot mesh: elite_robot

View File

@@ -114,11 +114,12 @@ gripper.misumi_rz:
goal: goal:
properties: properties:
data: data:
type: string type: object
required: required:
- data - data
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: modbus_crc参数 title: modbus_crc参数
@@ -398,30 +399,26 @@ gripper.misumi_rz:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -504,71 +501,82 @@ gripper.mock:
type: UniLabJsonCommand type: UniLabJsonCommand
push_to: push_to:
feedback: feedback:
effort: torque effort: effort
position: position position: position
reached_goal: reached_goal
stalled: stalled
goal: goal:
command.max_effort: torque command: command
command.position: position position: position
torque: torque
velocity: velocity
goal_default: goal_default:
command: command:
max_effort: 0.0 max_effort: 0.0
position: 0.0 position: 0.0
handles: {} handles: {}
placeholder_keys: {}
result: result:
effort: torque effort: effort
position: position position: position
reached_goal: reached_goal
stalled: stalled
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
effort: effort:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
reached_goal: reached_goal:
type: boolean type: boolean
stalled: stalled:
type: boolean type: boolean
required:
- position
- effort
- stalled
- reached_goal
title: GripperCommand_Feedback title: GripperCommand_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
additionalProperties: false
properties: properties:
max_effort: max_effort:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- position - position
- max_effort - max_effort
title: command title: command
type: object type: object
required:
- command
title: GripperCommand_Goal title: GripperCommand_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
effort: effort:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
reached_goal: reached_goal:
type: boolean type: boolean
stalled: stalled:
type: boolean type: boolean
required:
- position
- effort
- stalled
- reached_goal
title: GripperCommand_Result title: GripperCommand_Result
type: object type: object
required: required:
@@ -604,8 +612,8 @@ gripper.mock:
type: number type: number
required: required:
- position - position
- velocity
- torque
- status - status
- torque
- velocity
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -24,6 +24,27 @@ linear_motion.grbl:
title: initialize参数 title: initialize参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-list:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: list的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: list参数
type: object
type: UniLabJsonCommand
auto-set_position: auto-set_position:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -93,44 +114,39 @@ linear_motion.grbl:
type: UniLabJsonCommandAsync type: UniLabJsonCommandAsync
move_through_points: move_through_points:
feedback: feedback:
current_pose.pose.position: position current_pose: current_pose
estimated_time_remaining.sec: time_remaining distance_remaining: distance_remaining
navigation_time.sec: time_spent estimated_time_remaining: estimated_time_remaining
number_of_poses_remaining: pose_number_remaining navigation_time: navigation_time
number_of_poses_remaining: number_of_poses_remaining
number_of_recoveries: number_of_recoveries
goal: goal:
poses[].pose.position: positions[] behavior_tree: behavior_tree
poses: poses
positions: positions
goal_default: goal_default:
behavior_tree: '' behavior_tree: ''
poses: poses: []
- header:
frame_id: ''
stamp:
nanosec: 0
sec: 0
pose:
orientation:
w: 1.0
x: 0.0
y: 0.0
z: 0.0
position:
x: 0.0
y: 0.0
z: 0.0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
result: result
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
current_pose: current_pose:
additionalProperties: false
properties: properties:
header: header:
additionalProperties: false
properties: properties:
frame_id: frame_id:
type: string type: string
stamp: stamp:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -151,16 +167,26 @@ linear_motion.grbl:
title: header title: header
type: object type: object
pose: pose:
additionalProperties: false
properties: properties:
orientation: orientation:
additionalProperties: false
properties: properties:
w: w:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -170,12 +196,19 @@ linear_motion.grbl:
title: orientation title: orientation
type: object type: object
position: position:
additionalProperties: false
properties: properties:
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -194,8 +227,11 @@ linear_motion.grbl:
title: current_pose title: current_pose
type: object type: object
distance_remaining: distance_remaining:
maximum: 3.4028235e+38
minimum: -3.4028235e+38
type: number type: number
estimated_time_remaining: estimated_time_remaining:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -211,6 +247,7 @@ linear_motion.grbl:
title: estimated_time_remaining title: estimated_time_remaining
type: object type: object
navigation_time: navigation_time:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -233,16 +270,10 @@ linear_motion.grbl:
maximum: 32767 maximum: 32767
minimum: -32768 minimum: -32768
type: integer type: integer
required:
- current_pose
- navigation_time
- estimated_time_remaining
- number_of_recoveries
- distance_remaining
- number_of_poses_remaining
title: NavigateThroughPoses_Feedback title: NavigateThroughPoses_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
behavior_tree: behavior_tree:
type: string type: string
@@ -256,12 +287,8 @@ linear_motion.grbl:
stamp: stamp:
properties: properties:
nanosec: nanosec:
maximum: 4294967295
minimum: 0
type: integer type: integer
sec: sec:
maximum: 2147483647
minimum: -2147483648
type: integer type: integer
required: required:
- sec - sec
@@ -314,23 +341,17 @@ linear_motion.grbl:
required: required:
- header - header
- pose - pose
title: poses
type: object type: object
type: array type: array
required:
- poses
- behavior_tree
title: NavigateThroughPoses_Goal title: NavigateThroughPoses_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
result: result:
properties: {} additionalProperties: true
required: []
title: result title: result
type: object type: object
required:
- result
title: NavigateThroughPoses_Result title: NavigateThroughPoses_Result
type: object type: object
required: required:
@@ -340,9 +361,15 @@ linear_motion.grbl:
type: NavigateThroughPoses type: NavigateThroughPoses
set_spindle_speed: set_spindle_speed:
feedback: feedback:
position: spindle_speed error: error
header: header
position: position
velocity: velocity
goal: goal:
position: spindle_speed max_velocity: max_velocity
min_duration: min_duration
position: position
spindle_speed: spindle_speed
goal_default: goal_default:
max_velocity: 0.0 max_velocity: 0.0
min_duration: min_duration:
@@ -350,19 +377,25 @@ linear_motion.grbl:
sec: 0 sec: 0
position: 0.0 position: 0.0
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
error: error:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
header: header:
additionalProperties: false
properties: properties:
frame_id: frame_id:
type: string type: string
stamp: stamp:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -383,21 +416,24 @@ linear_motion.grbl:
title: header title: header
type: object type: object
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
velocity: velocity:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- header
- position
- velocity
- error
title: SingleJointPosition_Feedback title: SingleJointPosition_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
max_velocity: max_velocity:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
min_duration: min_duration:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -413,16 +449,13 @@ linear_motion.grbl:
title: min_duration title: min_duration
type: object type: object
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- position
- min_duration
- max_velocity
title: SingleJointPosition_Goal title: SingleJointPosition_Goal
type: object type: object
result: result:
properties: {} additionalProperties: true
required: []
title: SingleJointPosition_Result title: SingleJointPosition_Result
type: object type: object
required: required:
@@ -432,7 +465,7 @@ linear_motion.grbl:
type: SingleJointPosition type: SingleJointPosition
module: unilabos.devices.cnc.grbl_sync:GrblCNC module: unilabos.devices.cnc.grbl_sync:GrblCNC
status_types: status_types:
position: unilabos.messages:Point3D position: Point3D
spindle_speed: float spindle_speed: float
status: str status: str
type: python type: python
@@ -471,9 +504,9 @@ linear_motion.grbl:
status: status:
type: string type: string
required: required:
- status
- position - position
- spindle_speed - spindle_speed
- status
type: object type: object
version: 1.0.0 version: 1.0.0
linear_motion.toyo_xyz.sim: linear_motion.toyo_xyz.sim:
@@ -600,31 +633,6 @@ linear_motion.toyo_xyz.sim:
title: moveit_task参数 title: moveit_task参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: post_init的参数schema
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-resource_manager: auto-resource_manager:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -676,41 +684,41 @@ linear_motion.toyo_xyz.sim:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pick_and_place: pick_and_place:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -719,41 +727,41 @@ linear_motion.toyo_xyz.sim:
type: object type: object
type: SendCmd type: SendCmd
set_position: set_position:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -762,41 +770,41 @@ linear_motion.toyo_xyz.sim:
type: object type: object
type: SendCmd type: SendCmd
set_status: set_status:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -939,30 +947,26 @@ motor.iCL42:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -1000,8 +1004,8 @@ motor.iCL42:
success: success:
type: boolean type: boolean
required: required:
- motor_position
- is_executing_run - is_executing_run
- motor_position
- success - success
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -14,19 +14,24 @@ solid_dispenser.laiyu:
powder_tube_number: 0 powder_tube_number: 0
target_tube_position: '' target_tube_position: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
actual_mass_mg: actual_mass_mg actual_mass_mg: actual_mass_mg
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: SolidDispenseAddPowderTube_Feedback title: SolidDispenseAddPowderTube_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
compound_mass: compound_mass:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
powder_tube_number: powder_tube_number:
maximum: 2147483647 maximum: 2147483647
@@ -34,24 +39,19 @@ solid_dispenser.laiyu:
type: integer type: integer
target_tube_position: target_tube_position:
type: string type: string
required:
- powder_tube_number
- target_tube_position
- compound_mass
title: SolidDispenseAddPowderTube_Goal title: SolidDispenseAddPowderTube_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
actual_mass_mg: actual_mass_mg:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- actual_mass_mg
- success
title: SolidDispenseAddPowderTube_Result title: SolidDispenseAddPowderTube_Result
type: object type: object
required: required:
@@ -74,11 +74,12 @@ solid_dispenser.laiyu:
goal: goal:
properties: properties:
data: data:
type: string type: object
required: required:
- data - data
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: calculate_crc参数 title: calculate_crc参数
@@ -99,11 +100,12 @@ solid_dispenser.laiyu:
goal: goal:
properties: properties:
command: command:
type: string type: object
required: required:
- command - command
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: send_command参数 title: send_command参数
@@ -112,36 +114,37 @@ solid_dispenser.laiyu:
discharge: discharge:
feedback: {} feedback: {}
goal: goal:
float_input: float_input float_in: float_in
goal_default: goal_default:
float_in: 0.0 float_in: 0.0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: FloatSingleInput_Feedback title: FloatSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
float_in: float_in:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- float_in
title: FloatSingleInput_Goal title: FloatSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: FloatSingleInput_Result title: FloatSingleInput_Result
type: object type: object
required: required:
@@ -156,32 +159,31 @@ solid_dispenser.laiyu:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -200,38 +202,41 @@ solid_dispenser.laiyu:
y: 0.0 y: 0.0
z: 0.0 z: 0.0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: Point3DSeparateInput_Feedback title: Point3DSeparateInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- x
- y
- z
title: Point3DSeparateInput_Goal title: Point3DSeparateInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: Point3DSeparateInput_Result title: Point3DSeparateInput_Result
type: object type: object
required: required:
@@ -246,34 +251,33 @@ solid_dispenser.laiyu:
goal_default: goal_default:
int_input: 0 int_input: 0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: IntSingleInput_Feedback title: IntSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
int_input: int_input:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- int_input
title: IntSingleInput_Goal title: IntSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: IntSingleInput_Result title: IntSingleInput_Result
type: object type: object
required: required:
@@ -288,34 +292,33 @@ solid_dispenser.laiyu:
goal_default: goal_default:
int_input: 0 int_input: 0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: IntSingleInput_Feedback title: IntSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
int_input: int_input:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- int_input
title: IntSingleInput_Goal title: IntSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: IntSingleInput_Result title: IntSingleInput_Result
type: object type: object
required: required:
@@ -328,26 +331,25 @@ solid_dispenser.laiyu:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:

View File

@@ -34,7 +34,8 @@ chiller:
- register_address - register_address
- value - value
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: build_modbus_frame参数 title: build_modbus_frame参数
@@ -63,7 +64,8 @@ chiller:
required: required:
- temperature - temperature
type: object type: object
result: {} result:
type: integer
required: required:
- goal - goal
title: convert_temperature_to_modbus_value参数 title: convert_temperature_to_modbus_value参数
@@ -84,11 +86,12 @@ chiller:
goal: goal:
properties: properties:
data: data:
type: string type: object
required: required:
- data - data
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: modbus_crc参数 title: modbus_crc参数
@@ -116,42 +119,41 @@ chiller:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
set_temperature: set_temperature:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -266,9 +268,15 @@ heaterstirrer.dalong:
feedback: feedback:
status: status status: status
goal: goal:
pressure: pressure
purpose: purpose purpose: purpose
reflux_solvent: reflux_solvent
stir: stir
stir_speed: stir_speed
temp: temp temp: temp
temp_spec: temp_spec
time: time time: time
time_spec: time_spec
vessel: vessel vessel: vessel
goal_default: goal_default:
pressure: '' pressure: ''
@@ -301,20 +309,23 @@ heaterstirrer.dalong:
sample_id: '' sample_id: ''
type: '' type: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
message: message
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: HeatChill_Feedback title: HeatChill_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
pressure: pressure:
type: string type: string
@@ -325,8 +336,12 @@ heaterstirrer.dalong:
stir: stir:
type: boolean type: boolean
stir_speed: stir_speed:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
temp: temp:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
temp_spec: temp_spec:
type: string type: string
@@ -335,6 +350,7 @@ heaterstirrer.dalong:
time_spec: time_spec:
type: string type: string
vessel: vessel:
additionalProperties: false
properties: properties:
category: category:
type: string type: string
@@ -353,16 +369,26 @@ heaterstirrer.dalong:
parent: parent:
type: string type: string
pose: pose:
additionalProperties: false
properties: properties:
orientation: orientation:
additionalProperties: false
properties: properties:
w: w:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -372,12 +398,19 @@ heaterstirrer.dalong:
title: orientation title: orientation
type: object type: object
position: position:
additionalProperties: false
properties: properties:
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -407,20 +440,10 @@ heaterstirrer.dalong:
- data - data
title: vessel title: vessel
type: object type: object
required:
- vessel
- temp
- time
- temp_spec
- time_spec
- pressure
- reflux_solvent
- stir
- stir_speed
- purpose
title: HeatChill_Goal title: HeatChill_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
message: message:
type: string type: string
@@ -428,10 +451,6 @@ heaterstirrer.dalong:
type: string type: string
success: success:
type: boolean type: boolean
required:
- success
- message
- return_info
title: HeatChill_Result title: HeatChill_Result
type: object type: object
required: required:
@@ -440,42 +459,42 @@ heaterstirrer.dalong:
type: object type: object
type: HeatChill type: HeatChill
set_temp_target: set_temp_target:
feedback: {} feedback:
status: status
goal: goal:
command: temp command: command
temp: temp
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -484,42 +503,42 @@ heaterstirrer.dalong:
type: object type: object
type: SendCmd type: SendCmd
set_temp_warning: set_temp_warning:
feedback: {} feedback:
status: status
goal: goal:
command: temp command: command
temp: temp
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -569,8 +588,8 @@ heaterstirrer.dalong:
- status - status
- stir_speed - stir_speed
- temp - temp
- temp_warning
- temp_target - temp_target
- temp_warning
type: object type: object
version: 1.0.0 version: 1.0.0
tempsensor: tempsensor:
@@ -691,42 +710,41 @@ tempsensor:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
set_warning: set_warning:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:

Some files were not shown because too many files have changed in this diff Show More