158 Commits

Author SHA1 Message Date
Calvin Cao
d2a30fe33b Merge pull request #177 from sun7151887/yb4-fix
Yb4默认仿真机
2025-11-27 18:49:41 +08:00
dijkstra402
096875e910 默认仿真机 2025-11-27 18:22:46 +08:00
Calvin Cao
2e17dee121 Merge pull request #167 from lixinyu1011/workstation_dev_YB4
解决奔耀输入配方的,电解液体积为小数的问题
2025-11-16 17:36:50 +08:00
lixinyu1011
c03abb341a 解决奔耀输入配方的,电解液体积为小数的问题 2025-11-16 16:24:59 +08:00
dijkstra402
ee4ed26846 Merge branch 'workstation_dev_YB4' of https://github.com/dptech-corp/Uni-Lab-OS into workstation_dev_YB4 2025-11-11 09:54:22 +08:00
calvincao
b97be6a5d4 feat(battery): 更新电池工作站配置与物料布局
- 修改弹夹尺寸默认值,确保非空时使用实际值
- 调整new_cellconfig3c.json中设备位置和尺寸配置
- 更新CoinCellDeck的尺寸和原点坐标
-重新分配所有物料和弹夹的位置坐标
- 调整电解液缓存位和回收位坐标
- 更新物料板和tip box的布局位置
2025-11-10 21:40:02 +08:00
Calvin Cao
44f830cf00 Merge pull request #163 from sun7151887/yb4-fix
更新YB_Deck堆栈坐标位置,根据图片像素坐标映射到实际尺寸
2025-11-10 19:30:26 +08:00
dijkstra402
04b578a68b 更新YB_Deck堆栈坐标位置,根据图片像素坐标映射到实际尺寸 2025-11-10 18:57:20 +08:00
dijkstra402
19dffcb5db 更新YB_Deck堆栈坐标位置,根据图片像素坐标映射到实际尺寸 2025-11-10 18:57:10 +08:00
dijkstra402
b441362cd2 Merge branch 'workstation_dev_YB4' of https://github.com/dptech-corp/Uni-Lab-OS into workstation_dev_YB4 2025-11-10 18:35:41 +08:00
dijkstra402
ed53ef2f64 Update bioyond_cell and YAML configurations: modified default Excel paths and added new bottle carrier resources. Removed unused fields and updated descriptions for clarity. 2025-11-10 18:35:37 +08:00
dijkstra402
0c9f26e8fc Update Excel files: modified bioyond_cell and material_template with new data 2025-11-10 18:35:21 +08:00
calvincao
39a799cabd feat(device): 更新设备配置文件路径和图标
- 修改 bioyond_cell.yaml 中的 xlsx 文件路径为用户目录路径- 在 bioyond_cell.yaml 中新增 warehouse_name 字段并设置默认值- 为 bioyond_cell.yaml 添加 resource_tree_transfer 参数结构定义
- 更新 bioyond_cell.yaml 中的状态类型和设备 ID 配置
- 将 coin_cell_workstation.yaml 的图标从 coin_cell_assembly_picture.webp 更改为 koudian.webp
- 移除 bioyond_cell.yaml 中冗余的 display_name 配置项
2025-11-10 18:28:38 +08:00
Junhan Chang
0d64563fb6 fix serialize for magazine 2025-11-10 15:40:29 +08:00
Calvin Cao
fbb9e0963d Merge pull request #162 from sun7151887/yb4-fix
Fix import: change electrodesheet to electrode_sheet
2025-11-10 13:38:16 +08:00
dijkstra402
af411ddfe6 Fix import: change electrodesheet to electrode_sheet
修改路径
2025-11-10 13:34:49 +08:00
calvincao
f5dbcb1bfc feat(bioyond_cell): 更新默认模板路径并添加温度字段- 更新了自动送料函数中的默认 Excel 模板路径- 在物料信息中新增 temperature 字段,默认值为0
- 更新了 create_orders 函数中的默认实验文件路径
- 注释掉了部分调试代码,保留关键示例和说明
- 添加了关于位置码、实验文件和物料模板的注释提示
2025-11-10 13:27:54 +08:00
calvincao
1ecf89ea27 修改excel 2025-11-10 13:21:56 +08:00
Calvin Cao
6efdf6e5a6 Merge pull request #161 from sun7151887/yb4-fix
Fix import: change electrodesheet to electrode_sheet
2025-11-09 22:35:10 +08:00
dijkstra402
e32dc55db0 Fix import: change electrodesheet to electrode_sheet 2025-11-09 22:02:17 +08:00
Calvin Cao
acc45b716d Merge pull request #160 from sun7151887/yb4-fix
Update coin cell assembly and YB_YH materials configuration
2025-11-09 21:44:42 +08:00
dijkstra402
017eaefb8d Update coin cell assembly and YB_YH materials configuration 2025-11-09 21:43:32 +08:00
Calvin Cao
9e8c692702 Merge pull request #159 from dptech-corp/workstation_dev_YB3
Update coin cell assembly configuration: change CSV file reference an…
2025-11-09 20:57:19 +08:00
calvincao
beb90f20d2 Update coin cell assembly configuration: change CSV file reference and modify resource names; enhance workstation initialization and packing functions. 2025-11-09 20:56:12 +08:00
Calvin Cao
7a284069d2 Merge pull request #158 from dptech-corp/workstation_dev_YB3
Workstation dev yb3
2025-11-09 17:12:41 +08:00
Calvin Cao
4a2d862333 Merge pull request #157 from sun7151887/fix/yb3-material-names-and-model
Update YB resources: add YB_ prefix to models and update deck configu…
2025-11-09 17:11:24 +08:00
dijkstra402
538891fcbe Update YB resources: add YB_ prefix to models and update deck configurations 2025-11-09 17:04:52 +08:00
Calvin Cao
a0e92b8e9b Merge pull request #156 from dptech-corp/workstation_dev_YB3
Workstation dev yb3
2025-11-09 15:48:35 +08:00
Calvin Cao
1d77225912 Merge branch 'workstation_dev_YB4' into workstation_dev_YB3 2025-11-09 15:48:22 +08:00
Calvin Cao
06e6ab0b7f Merge pull request #155 from sun7151887/fix/yb3-material-names-and-model
Fix warehouse mapping: use actual parent warehouse name instead of ha…
2025-11-09 15:15:55 +08:00
dijkstra402
5399c6c1cf Fix warehouse mapping: use actual parent warehouse name instead of hardcoded '手动堆栈' 2025-11-09 15:13:20 +08:00
Junhan Chang
f872d3ef56 add electrode_sheets definition, and fix magazines 2025-11-09 01:00:05 +08:00
Calvin Cao
85c6f4e688 Merge pull request #154 from lixinyu1011/workstation_dev_YB3
修改pymodbus和websocket的报送信息
2025-11-08 15:59:22 +08:00
lixinyu1011
442b759397 修改pymodbus和websocket的报送信息 2025-11-08 15:56:39 +08:00
Calvin Cao
47ecb154c8 Merge pull request #153 from sun7151887/fix/yb3-material-names-and-model
规范堆栈和瓶子的名称
2025-11-08 15:49:59 +08:00
dijkstra402
be429147c0 Fix infinite recursion in YB_jia_yang_tou_da by renaming carrier function to YB_jia_yang_tou_da_Carrier 2025-11-08 15:42:18 +08:00
Calvin Cao
123c69e97a Merge pull request #152 from lixinyu1011/workstation_dev_YB3
修改减少modbus报警信息,以及websocket报警信息
2025-11-08 15:21:33 +08:00
Calvin Cao
04004c9b6f Merge branch 'workstation_dev_YB3' into workstation_dev_YB3 2025-11-08 15:21:25 +08:00
lixinyu1011
45a778b928 修改减少modbus报警信息,以及websocket报警信息 2025-11-08 15:18:52 +08:00
Calvin Cao
c44ae32070 Merge pull request #151 from sun7151887/fix/yb3-material-names-and-model
Add debug prints to create_orders and add resource_tree_transfer method
2025-11-08 15:01:42 +08:00
dijkstra402
7af32b379b Add YB_ prefix to bottle carrier model names 2025-11-08 14:53:25 +08:00
Xuwznln
48d429ae00 fix resource_get param 2025-11-08 14:40:00 +08:00
Xuwznln
9bba4620b7 fix resource_get param 2025-11-08 14:39:36 +08:00
Xuwznln
d7494ca458 fix json dumps 2025-11-08 13:39:15 +08:00
Xuwznln
85dc46cd38 support name change during materials change 2025-11-08 13:39:13 +08:00
Xuwznln
5a0c2f9850 enable slave mode 2025-11-08 13:39:11 +08:00
Xuwznln
d897d70c3e change uuid logger to trace level 2025-11-08 13:39:09 +08:00
Xuwznln
d9dffc6bf8 correct remove_resource stats 2025-11-08 13:39:07 +08:00
Xuwznln
30b202bea0 disable slave connect websocket 2025-11-08 13:39:05 +08:00
Xuwznln
1b2c0dbcd7 adjust with_children param 2025-11-08 13:39:04 +08:00
Xuwznln
0f341e9b4d modify devices to use correct executor (sleep, create_task) 2025-11-08 13:39:01 +08:00
Xuwznln
4c3972820b support sleep and create_task in node 2025-11-08 13:39:00 +08:00
Xuwznln
a2a8ee9088 fix run async execution error 2025-11-08 13:39:00 +08:00
dijkstra402
200105f647 Add debug prints to create_orders and add resource_tree_transfer method 2025-11-08 13:35:47 +08:00
Xuwznln
8b5653d801 fix json dumps 2025-11-08 12:13:57 +08:00
Xuwznln
5f859917d4 support name change during materials change 2025-11-08 12:13:56 +08:00
Xuwznln
af2fb7f34a enable slave mode 2025-11-08 12:13:54 +08:00
Xuwznln
baa107c230 change uuid logger to trace level 2025-11-08 12:13:52 +08:00
Xuwznln
83854a741d correct remove_resource stats 2025-11-08 12:13:50 +08:00
Xuwznln
86c7880b5c disable slave connect websocket 2025-11-08 12:13:48 +08:00
Xuwznln
6d934e354c adjust with_children param 2025-11-08 12:13:46 +08:00
Xuwznln
bed453034f modify devices to use correct executor (sleep, create_task) 2025-11-08 12:13:44 +08:00
Xuwznln
5331d7bfba support sleep and create_task in node 2025-11-08 12:13:41 +08:00
Xuwznln
38ab7d3e78 fix run async execution error 2025-11-08 12:13:41 +08:00
Junhan Chang
966b51042d rename and fix all Yihua Materials: ClipMagazineHole→Magazine(ResourceStack), and use factory functions 2025-11-06 00:59:46 +08:00
Calvin Cao
d81638e20b Merge pull request #148 from lixinyu1011/workstation_dev_YB4
YB4branc_bylixinyu
2025-11-04 20:27:30 +08:00
lixinyu1011
3c583008aa YB4branc_bylixinyu 2025-11-04 20:19:27 +08:00
Calvin Cao
9a85bfddcd Merge pull request #147 from lixinyu1011/workstation_dev_YB3
1104_byxinyu
2025-11-04 03:57:57 +08:00
lixinyu1011
d4e1286df7 1104_byxinyu 2025-11-04 03:42:00 +08:00
calvincao
765038a136 Revert "Update YB_YH_materials.py"
This reverts commit bfd415279b.
2025-11-04 02:18:44 +08:00
Calvin Cao
1d4e4c8377 Merge pull request #146 from sun7151887/feature/update-yb-deck-coordinates
依华扣电工站物料信息正确
2025-11-04 02:05:36 +08:00
Calvin Cao
54f749bcdb Merge branch 'workstation_dev_YB4' into feature/update-yb-deck-coordinates 2025-11-04 02:05:18 +08:00
dijkstra402
16ad4bbecc 更新奔耀和依华工站的Deck坐标配置
- 更新奔耀YB工站deck坐标(基于图片像素精确计算)
  * 将粉末加样头堆栈拆分为左右两部分
  * 将试剂替换仓库拆分为左右两部分
  * 更新所有堆栈的坐标位置

- 更新依华扣电工站deck坐标(使用精确的像素-毫米转换)
  * 修正所有子弹夹的坐标位置(铝箔、正极片、正极壳等)
  * 更新料盘坐标(负极料盘、隔膜料盘)
  * 更新瓶架坐标(奔耀上料瓶架、电解液缓存位、回收位)
  * 更新枪头盒和废枪头盒坐标
  * 确保所有坐标在deck范围内(3650×1550mm)

- 转换比例说明:
  * 奔耀工站:deck左上角(206,446),使用1.56mm/像素
  * 依华工站:deck左上角(494,444)到右下角(2430,1608)
    X方向:1.885mm/像素,Y方向:1.332mm/像素
2025-11-04 02:01:44 +08:00
calvincao
0ad2eaafea Fix BottleRack references in CoincellDeck setup
- Updated references from bottle_rack_2x6 to bottle_rack_6x2 to align with the new configuration.
- Adjusted the loop for assigning ElectrodeSheets to use the correct BottleRack dimensions.
2025-11-04 01:57:30 +08:00
calvincao
1477384c1a Update CoinCellAssembly and YB_YH_materials configurations
- Adjusted CoincellDeck dimensions and origin coordinates for improved layout.
- Replaced CoincellDeck references with specific ClipMagazine instances in YB_YH_materials.py.
- Updated BottleRack configurations to reflect new item arrangements and dimensions.
2025-11-04 01:19:42 +08:00
Calvin Cao
8149a175d9 Merge pull request #145 from lixinyu1011/workstation_dev_YB3
Update YB_YH_materials.py
2025-11-04 00:41:17 +08:00
lixinyu1011
bfd415279b Update YB_YH_materials.py 2025-11-04 00:39:39 +08:00
Calvin Cao
0238a92e75 Merge pull request #144 from sun7151887/fix/yb3-material-names-and-model
更新YB工站deck坐标配置
2025-11-03 23:51:10 +08:00
dijkstra402
8009956326 更新YB工站deck坐标配置
- 根据实际布局图更新各堆栈的坐标位置
- 将粉末加样头堆栈拆分为左右两部分(10x1x1 -> 2个5x1x1)
- 将试剂替换仓库拆分为左右两部分(10x1x1 -> 2个5x1x1)
- 更新配液站内试剂仓库的坐标
- 所有坐标基于像素位置精确计算(deck原点: 206,446)
2025-11-03 23:49:02 +08:00
Calvin Cao
68fc4dd61e Merge pull request #143 from lixinyu1011/workstation_dev_YB3
1103-3byxinyu
2025-11-03 23:02:17 +08:00
lixinyu1011
cd12932788 1103byxinyu 2025-11-03 22:53:37 +08:00
calvincao
f230028558 feat: Enhance CoincellDeck setup with new ClipMagazine and BottleRack configurations
- Refactored ClipMagazine class to inherit from ItemizedResource and updated hole dimensions.
- Introduced ClipMagazine_four class for a new 2x2 hole layout.
- Expanded CoincellDeck setup to include multiple ClipMagazines and MaterialPlates with ElectrodeSheets.
- Improved BottleRack initialization with dynamic item positioning and resource assignment.
- Added serialization methods for new classes to maintain state consistency.
2025-11-03 21:30:27 +08:00
Calvin Cao
1c1a6b16c8 Merge pull request #142 from lixinyu1011/workstation_dev_YB3
11103-2byxinyu
2025-11-03 19:51:56 +08:00
lixinyu1011
a2d6012080 Merge branch 'workstation_dev_YB3' of https://github.com/lixinyu1011/Uni-Lab-OS into workstation_dev_YB3 2025-11-03 19:50:04 +08:00
lixinyu1011
10adc853a5 1103-2byxinyu 2025-11-03 19:50:01 +08:00
Calvin Cao
69ec034623 Merge pull request #141 from lixinyu1011/workstation_dev_YB3
1103byxinyu
2025-11-03 19:47:13 +08:00
Calvin Cao
62d08aa954 Merge branch 'workstation_dev_YB3' into workstation_dev_YB3 2025-11-03 19:46:52 +08:00
lixinyu1011
4485907df8 1103byxinyu 2025-11-03 18:46:50 +08:00
calvincao
b5b2358967 fix: 更新HTTP服务配置和物料类型映射
- 修改BIOYOND_HTTP_HOST的默认值为新的IP地址172.21.32.91
- 调整物料类型映射中“加样头(大)”的UUID顺序,并注释掉“加样头(大)板”配置
2025-11-03 18:20:50 +08:00
lixinyu1011
11f4f44bf9 Update coin_cell_assembly.py 2025-11-03 16:51:28 +08:00
lixinyu1011
f52fbd650e Update bioyond_cell_workstation.py 2025-11-03 16:50:59 +08:00
calvincao
e561c818b8 feat: 添加多个新仓库配置到config.py
- 新增多个仓库配置,包括大分液瓶堆栈、小分液瓶堆栈、站内Tip头盒堆栈等
- 每个仓库配置包含UUID和站点UUID映射
2025-11-03 14:31:50 +08:00
Calvin Cao
5cbd880e5a Merge pull request #140 from sun7151887/fix/yb3-material-names-and-model
fix: 修正YB warehouse排列方式和物料类型映射
2025-11-01 11:16:00 +08:00
Calvin Cao
41e7251f62 Merge branch 'workstation_dev_YB3' into fix/yb3-material-names-and-model 2025-11-01 11:14:45 +08:00
dijkstra402
727d2c2595 fix: 修正YB warehouse排列方式和物料类型映射
- 修改warehouse_factory为YB_warehouse_factory
- 调整warehouse排列方式:左上角为A01,竖着排ABCD,横着排01、02、03
- 修正config.py中的物料名称拼写错误(YB_fen_ye_20ml_Bottle, YB_pei_ye_xiao_Bottle)
- 添加缺失的warehouse函数(bioyond_warehouse_2x2x1, bioyond_warehouse_3x5x1, bioyond_warehouse_20x1x1)
- 更新decks.py中的warehouse位置映射
- 删除废弃的bottles.py和warehouses.py文件
2025-11-01 10:42:31 +08:00
Calvin Cao
202a2667fd Merge pull request #139 from lixinyu1011/workstation_dev_YB3
byxinyu111
2025-11-01 10:41:13 +08:00
lixinyu1011
03745c5d08 byxinyu111 2025-11-01 10:37:45 +08:00
Calvin Cao
385a495e21 Merge pull request #138 from lixinyu1011/workstation_dev_YB3
新建入库物料系统
2025-10-31 19:04:28 +08:00
lixinyu1011
91513a5f4c Delete button_battery_station.py 2025-10-31 19:02:06 +08:00
lixinyu1011
a62896eda2 1031_byxinyu 2025-10-31 18:57:38 +08:00
lixinyu1011
a82d1b7bdb Merge remote-tracking branch 'upstream/workstation_dev_YB3' into workstation_dev_YB3 2025-10-31 15:30:28 +08:00
lixinyu1011
6d7c39da9e 1031 2025-10-31 15:29:59 +08:00
Calvin Cao
d8e9ad4413 Merge pull request #136 from sun7151887/fix/yb3-material-names-and-model
fix: 更新物料类型配置映射
2025-10-31 15:13:48 +08:00
dijkstra402
eb93b83415 fix: 更新物料类型配置映射 2025-10-31 15:05:47 +08:00
lixinyu1011
6df93a5db7 Merge remote-tracking branch 'upstream/workstation_dev_YB3' into workstation_dev_YB3 2025-10-31 14:02:45 +08:00
lixinyu1011
2eb9986edb 123 2025-10-31 13:54:58 +08:00
calvincao
fe4e49e56d feat(workstation): 更新 Bioyond 和 Coin Cell 组装工作站配置
- 修改 Bioyond Studio 配置文件中的 API 主机地址
- 更新 bioyond_cell_workstation.py 中的默认模板路径
- 新增物料模板文件 material_template.xlsx
- 扩展 func_pack_send_msg_cmd 函数以支持 assembly_pressure 参数
- 更新 coin_cell_workstation.yaml 文件以包含 assembly_pressure 的默认值和类型定义
2025-10-31 13:53:58 +08:00
calvincao
0fba4cf275 feat(unilabos): 更新设备配置和资源定义
- 修改了 bioyond_cell.yaml 中的 xlsx_path 路径分隔符为反斜杠- 在 bioyond_cell.yaml 中新增多个自动命令定义,包括创建物料、处理报告和调度重置等功能- 修改 coin_cell_assembly.py 中 func_pack_send_msg_cmd 函数签名并调整调用参数
- 新增 qiming_coin_cell_code 方法用于设置启明扣电配置参数
- 更新 coin_cell_assembly_a.csv 文件中的寄存器描述和新增压制模式及清洁忽略选项- 修改 bioyond_studio 配置文件中的默认 API 主机地址
- 更新 new_cellconfig3c.json 中的设备类名为 coincellassemblyworkstation_device- 删除 reaction_station_bioyond.yaml 的全部内容,仅保留空对象
-重新组织 YB_bottle.yaml 和 YB_bottle_carriers.yaml 中的资源分类和命名定义
2025-10-30 19:56:34 +08:00
Calvin Cao
ef9359776a Merge pull request #134 from sun7151887/fix/yb3-material-names-and-model
feat: 添加扣电工作站 setup() 方法并修复显示问题
2025-10-30 16:31:50 +08:00
Calvin Cao
954f1ee7b2 Merge branch 'workstation_dev_YB3' into fix/yb3-material-names-and-model 2025-10-30 16:31:27 +08:00
dijkstra402
f58921ef82 feat: 添加扣电工作站 setup() 方法并修复显示问题
主要改动:
-  在 CoincellDeck 实现 setup() 方法(模仿 decks.py 三步配置模式)
-  统一 Deck 默认尺寸为 1000x1000x900mm
-  优化料盘布局:横向排列,留50mm边距
-  简化工作站 deck 创建逻辑(从30行减至1行)
-  新增 create_coin_cell_deck() 便捷函数
-  修复 ClipMagazine 参数错误
-  删除约200行冗余代码
-  修复底座不显示问题

技术细节:
- MaterialPlate 位置: liaopan1(50,50), liaopan2(250,50), 电池料盘(450,50)
- 自动为 liaopan1 添加16个初始极片
- 支持3种 deck 创建方式
- 智能判断是否需要 setup
2025-10-30 16:18:43 +08:00
calvincao
95bdd39bf8 fix(workstation): 更新 coin_cell_assembly_a.csv 中的寄存器和线圈定义为中文描述
- 将寄存器和线圈定义中的英文描述替换为中文,提升可读性
- 确保所有定义格式一致,保持文件的整洁性和维护性
2025-10-30 14:25:33 +08:00
calvincao
b3e28196c6 feat(battery): 更新电池工位资源配置
- 将 coin_cell_deck 类型从 container 更改为 coin_cell_deck
- 将 material_plate 类型从 container 更改为 material_plate
- 将 material_hole 类型从 container 更改为 material_hole- 移除电极片容器结构,直接使用 electrode_sheet 类型
- 更新物料孔配置参数,包括直径、深度和最大片数
-重新组织料盘结构,明确父子节点关系
- 添加新的电极片定义并关联到对应的物料孔
- 调整所有物料孔坐标位置以匹配新布局
- 为 liaopan2 添加完整的子节点结构和排序规则
2025-10-30 11:22:17 +08:00
calvincao
9fe8f4f28f fix(workstation): 修复 coin_cell_assembly_a.csv 文件中的寄存器和线圈定义格式
- 重新排列并清理了 coin_cell_assembly_a.csv 中的寄存器和线圈定义
- 确保所有定义的格式一致,提升可读性和维护性
2025-10-29 21:36:52 +08:00
calvincao
39bc317bfc feat(workstation): 支持多种输入类型的 station_resource 并优化物料系统初始化
- 新增 `_coerce_station_resource_input` 函数以支持 dict、list 和其他类型转换为 Deck
- 添加对 Modbus 客户端方法的兼容性封装,确保 slave/unit 参数正确传递- 在初始化时根据 station_resource 动态创建或赋值 deck
- 自动构建默认物料台面及三个料盘,并分配初始电极片资源
- 移除旧有的硬编码物料系统注释代码
- 更新资源导出逻辑以使用工作站实例中的 deck 属性
2025-10-29 18:39:22 +08:00
calvincao
a130c03ebd feat(workstation): 移除旧版bioyond设备配置并优化扣电组装工作站- 删除bioyond.yaml和bioyond_dispensing_station.yaml旧设备配置文件- 优化扣电组装工作站配置,移除不必要的子资源引用- 更新Modbus通信地址和端口配置- 简化CoinCellAssemblyWorkstation类的初始化参数- 移除冗余的deck资源创建逻辑
- 更新反应站配置文件中drip_back命令的位置
- 添加新的Modbus寄存器和线圈定义
- 移除workstation_base.py基类文件
2025-10-29 10:44:30 +08:00
calvincao
a97781c4eb Merge remote-tracking branch 'origin/dev' into workstation_dev_YB3 2025-10-28 11:47:07 +08:00
calvincao
c35edcece1 重构 coin_cell_assembly 目录结构 2025-10-28 11:42:14 +08:00
Calvin Cao
524e0f3053 Merge pull request #132 from sun7151887/fix/yb3-material-names-and-model
feat: 添加YB瓶子和载架配置
2025-10-27 22:30:40 +08:00
Calvin Cao
66f483929d Merge branch 'workstation_dev_YB3' into fix/yb3-material-names-and-model 2025-10-27 22:30:16 +08:00
dijkstra402
2d58576937 feat: 添加YB瓶子和载架配置
- 在YB_bottles.py中添加8种瓶子类型(100ml液体、高粘液、5ml分液瓶、20ml分液瓶、配液瓶小、配液瓶大、枪头等)
- 在YB_bottle_carriers.py中添加12个载架函数(包括新增的高粘液载架和100ml液体载架)
- 更新config.py的MATERIAL_TYPE_MAPPINGS配置,添加16种物料类型映射
- 创建YB_bottle_carriers.yaml注册文件,包含所有载架和瓶子函数
- 创建YB_bottle.yaml注册文件,包含独立的瓶子函数配置
- 移除不存在的瓶子函数引用(YB_Solid_Vial等4个函数)
2025-10-27 22:23:09 +08:00
calvincao
ff25e814de feat: add new glove box internal stack configuration with site UUIDs 2025-10-27 22:08:02 +08:00
Calvin Cao
0163d16cbb Merge pull request #131 from lixinyu1011/workstation_dev_YB3
by_Xinyu1027
2025-10-27 20:15:43 +08:00
lixinyu1011
3231d60646 1027by_Xinyu 2025-10-27 20:08:19 +08:00
lixinyu1011
d0279f63f0 Merge remote-tracking branch 'upstream/workstation_dev_YB3' into workstation_dev_YB3 2025-10-27 19:33:45 +08:00
lixinyu1011
ceef342860 1027byxinyu 2025-10-27 18:16:26 +08:00
h840473807
42f7010134 提交扣电工站最新代码到YB3分支
提交扣电工站最新代码到YB3分支,更新注册表
2025-10-27 11:57:57 +08:00
calvincao
190b2d2518 清理扣电不必要代码 2025-10-27 11:43:03 +08:00
calvincao
2901d72b4b feat: add button battery assembly station resources and configuration files
- Introduced new Python modules for button battery assembly, including resource classes and configurations.
- Added JSON and CSV files for resource definitions and device configurations.
- Created initial setup for the coin cell assembly workstation, including material handling and resource management.
2025-10-25 13:50:41 +08:00
calvincao
6ad0157b50 feat: add new warehouse configurations and update site UUIDs in bioyond_studio config 2025-10-24 16:37:11 +08:00
calvincao
55b678cd37 fix: update report IP address in configuration and clean up parameters in SOLID_LIQUID_MAPPINGS 2025-10-24 14:22:39 +08:00
Calvin Cao
8101a22a0f Merge pull request #130 from sun7151887/workstation_dev_YB3
refactor: 将 BIOYOND_PolymerStation_ 前缀统一改为 YB_
2025-10-24 13:56:08 +08:00
Calvin Cao
667138baac Merge branch 'workstation_dev_YB3' into workstation_dev_YB3 2025-10-24 13:56:00 +08:00
dijkstra402
01adf7ca92 refactor: 将 BIOYOND_PolymerStation_ 前缀统一改为 YB_
- 重命名 bottles.py 中所有工厂函数:BIOYOND_PolymerStation_* -> YB_*
- 重命名 bottle_carriers.py 中所有载具工厂函数和导入
- 更新 registry YAML 文件中的 module 引用
- 更新 MATERIAL_TYPE_MAPPINGS 配置中的类型字符串
- 更新测试文件和样例 JSON 中的类型引用
- 添加 YB_* 别名条目到 registry 以支持双键访问
2025-10-24 13:49:48 +08:00
Calvin Cao
f606062696 Merge pull request #129 from lixinyu1011/workstation_dev_YB3
xinyu1024修改
2025-10-24 11:44:16 +08:00
Calvin Cao
67d1c4acce Merge branch 'workstation_dev_YB3' into workstation_dev_YB3 2025-10-24 11:44:04 +08:00
lixinyu1011
7206e42bf1 xinyu1024修改 2025-10-24 11:37:36 +08:00
calvincao
e92d933968 refactor(bioyond_cell_workstation): 重构物料创建与入库逻辑- 移除从CSV读取物料名称的功能
- 新增通过参数传递物料名称列表的方式- 抽离仓库位置加载逻辑至独立方法
- 简化物料创建与入库流程- 统一使用资源同步器进行数据同步
- 更新调用示例以适配新接口
2025-10-23 22:36:21 +08:00
Calvin Cao
f0ebcc60bb Merge pull request #126 from sun7151887/fix/yb3-material-names-and-model
添加新物料类型映射:包括100ml液体、液、高粘液、5ml/20ml分液瓶、配液瓶、加样头、适配器块、枪头盒等
2025-10-23 21:59:26 +08:00
dijkstra402
e2097f0b22 添加新物料类型映射:包括100ml液体、液、高粘液、5ml/20ml分液瓶、配液瓶、加样头、适配器块、枪头盒等 2025-10-23 21:56:54 +08:00
calvincao
fd73731130 增强批量入库功能,添加物料数据同步逻辑;优化日志记录以提供更详细的同步状态信息。 2025-10-23 18:02:49 +08:00
Calvin Cao
ab7f2081c9 Merge pull request #124 from sun7151887/fix/yb3-material-names-and-model
更新载架网格布局:5ml/20ml/配液瓶(小)板改为4x2,加样头(大)板改为1x1
2025-10-23 17:45:29 +08:00
dijkstra402
9e850d8a81 更新载架网格布局:5ml/20ml/配液瓶(小)板改为4x2,加样头(大)板改为1x1 2025-10-23 17:42:10 +08:00
calvincao
1af6ffafc6 新增批量创建固体物料和从CSV文件入库的功能;更新配置文件中的 report_ip 默认值;新增 solid_materials.csv 文件以支持物料名称导入。 2025-10-23 17:32:08 +08:00
Calvin Cao
35fc2f5ea6 Merge pull request #123 from sun7151887/fix/yb3-material-names-and-model
fix(yb3): 物料名称与模型对齐;YAML 去掉 BIOYOND_PolymerStation_ 前缀;修复 6StockCarri…
2025-10-23 15:43:40 +08:00
dijkstra402
d3d8ba6500 fix(yb3): 物料名称与模型对齐;YAML 去掉 BIOYOND_PolymerStation_ 前缀;修复 6StockCarrier model 2025-10-23 15:32:36 +08:00
calvincao
5a7845d8ca 更新配置文件中的 report_ip 默认值,优化 bioyond_cell_workstation.py 中的订单状态处理逻辑,新增多个瓶子和载架类型的定义,调整仓库结构以支持更灵活的物料管理。 2025-10-23 08:34:33 +08:00
calvincao
9c4d0256cf 增强配置文件,新增 report_ip 选项以支持本机 IP 地址的灵活配置;在 bioyond_cell_workstation.py 中优化推送地址更新逻辑,支持自动检测和配置优先级处理。 2025-10-22 16:38:32 +08:00
calvincao
de7c80c3c2 重构:完善配置加载机制与初始化逻辑
新增环境变量覆盖机制,增强配置灵活性

优化 bioyond_rpc.py 与 bioyond_cell_workstation.py 的初始化流程与结构

修正 station.py 工作流映射逻辑,确保正确性

提高代码可读性与模块间解耦程度
2025-10-22 16:13:36 +08:00
calvincao
e70c545ec8 修复 **bioyond_yihua_YB.json** 中的 JSON 合并冲突,清理不必要的标记。 2025-10-21 15:19:44 +08:00
calvincao
2c2d1e5569 在 **bioyond_cell_workstation.py** 中实现 update_push_ip 方法并增强错误处理;修复 **bioyond_yihua_YB.json** 中的 JSON 合并冲突。 2025-10-21 14:58:38 +08:00
Calvin Cao
4638611fe7 Merge pull request #119 from lixinyu1011/workstation_dev_YB3
Update station.py
2025-10-21 14:51:54 +08:00
lixinyu1011
37641c4389 xinyu1021推送代码 2025-10-21 14:48:55 +08:00
lixinyu1011
ab697ce973 Update station.py 2025-10-20 16:12:38 +08:00
Calvin Cao
d4724b8664 Merge pull request #117 from lixinyu1011/workstation_dev_YB3
Update bioyond_cell_workstation.py
2025-10-20 15:33:17 +08:00
lixinyu1011
2f25063bf1 Update bioyond_cell_workstation.py 2025-10-20 15:30:41 +08:00
Calvin Cao
00b4b9cd87 Merge pull request #116 from lixinyu1011/workstation_dev_YB3
1020_YB奔耀仿真机同步对齐dev_unilab可控
2025-10-20 12:56:36 +08:00
lixinyu1011
d2352cc514 1020_YB奔耀仿真机同步对齐dev_unilab可控
待修改unilab的http服务
2025-10-20 12:48:19 +08:00
86 changed files with 25237 additions and 14268 deletions

14473
bioyond_yihua_YB.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,6 @@ extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
"sphinx_rtd_theme",
"sphinxcontrib.mermaid"
]
source_suffix = {
@@ -43,8 +42,6 @@ myst_enable_extensions = [
"substitution",
]
myst_fence_as_directive = ["mermaid"]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
@@ -206,5 +203,3 @@ def generate_action_includes(app):
def setup(app):
app.connect("builder-inited", generate_action_includes)
app.add_js_file("https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js")
app.add_js_file(None, body="mermaid.initialize({startOnLoad:true});")

View File

@@ -1,26 +1,88 @@
## 简单单变量动作函数
### `SendCmd`
```{literalinclude} ../../unilabos_msgs/action/SendCmd.action
:language: yaml
```
----
---
### `StrSingleInput`
```{literalinclude} ../../unilabos_msgs/action/StrSingleInput.action
:language: yaml
```
---
### `IntSingleInput`
```{literalinclude} ../../unilabos_msgs/action/IntSingleInput.action
:language: yaml
```
---
### `FloatSingleInput`
```{literalinclude} ../../unilabos_msgs/action/FloatSingleInput.action
:language: yaml
```
---
### `Point3DSeparateInput`
```{literalinclude} ../../unilabos_msgs/action/Point3DSeparateInput.action
:language: yaml
```
---
### `Wait`
```{literalinclude} ../../unilabos_msgs/action/Wait.action
:language: yaml
```
---
## 常量有机化学操作
Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
### `Clean`
```{literalinclude} ../../unilabos_msgs/action/Clean.action
:language: yaml
```
----
---
### `EvacuateAndRefill`
```{literalinclude} ../../unilabos_msgs/action/EvacuateAndRefill.action
:language: yaml
```
---
### `Evaporate`
```{literalinclude} ../../unilabos_msgs/action/Evaporate.action
:language: yaml
```
---
### `HeatChill`
```{literalinclude} ../../unilabos_msgs/action/HeatChill.action
:language: yaml
```
---
### `HeatChillStart`
@@ -28,7 +90,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `HeatChillStop`
@@ -36,7 +98,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `PumpTransfer`
@@ -44,12 +106,195 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
----
---
### `Separate`
```{literalinclude} ../../unilabos_msgs/action/Separate.action
:language: yaml
```
---
### `Stir`
```{literalinclude} ../../unilabos_msgs/action/Stir.action
:language: yaml
```
---
### `Add`
```{literalinclude} ../../unilabos_msgs/action/Add.action
:language: yaml
```
---
### `AddSolid`
```{literalinclude} ../../unilabos_msgs/action/AddSolid.action
:language: yaml
```
---
### `AdjustPH`
```{literalinclude} ../../unilabos_msgs/action/AdjustPH.action
:language: yaml
```
---
### `Centrifuge`
```{literalinclude} ../../unilabos_msgs/action/Centrifuge.action
:language: yaml
```
---
### `CleanVessel`
```{literalinclude} ../../unilabos_msgs/action/CleanVessel.action
:language: yaml
```
---
### `Crystallize`
```{literalinclude} ../../unilabos_msgs/action/Crystallize.action
:language: yaml
```
---
### `Dissolve`
```{literalinclude} ../../unilabos_msgs/action/Dissolve.action
:language: yaml
```
---
### `Dry`
```{literalinclude} ../../unilabos_msgs/action/Dry.action
:language: yaml
```
---
### `Filter`
```{literalinclude} ../../unilabos_msgs/action/Filter.action
:language: yaml
```
---
### `FilterThrough`
```{literalinclude} ../../unilabos_msgs/action/FilterThrough.action
:language: yaml
```
---
### `Hydrogenate`
```{literalinclude} ../../unilabos_msgs/action/Hydrogenate.action
:language: yaml
```
---
### `Purge`
```{literalinclude} ../../unilabos_msgs/action/Purge.action
:language: yaml
```
---
### `Recrystallize`
```{literalinclude} ../../unilabos_msgs/action/Recrystallize.action
:language: yaml
```
---
### `RunColumn`
```{literalinclude} ../../unilabos_msgs/action/RunColumn.action
:language: yaml
```
---
### `StartPurge`
```{literalinclude} ../../unilabos_msgs/action/StartPurge.action
:language: yaml
```
---
### `StartStir`
```{literalinclude} ../../unilabos_msgs/action/StartStir.action
:language: yaml
```
---
### `StopPurge`
```{literalinclude} ../../unilabos_msgs/action/StopPurge.action
:language: yaml
```
---
### `StopStir`
```{literalinclude} ../../unilabos_msgs/action/StopStir.action
:language: yaml
```
---
### `Transfer`
```{literalinclude} ../../unilabos_msgs/action/Transfer.action
:language: yaml
```
---
### `WashSolid`
```{literalinclude} ../../unilabos_msgs/action/WashSolid.action
:language: yaml
```
---
## 移液工作站及相关生物自动化设备操作
Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。
### `LiquidHandlerAspirate`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAspirate.action
:language: yaml
```
---
### `LiquidHandlerDiscardTips`
@@ -57,7 +302,15 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerDispense`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDispense.action
:language: yaml
```
---
### `LiquidHandlerDropTips`
@@ -65,7 +318,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerDropTips96`
@@ -73,7 +326,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMoveLid`
@@ -81,7 +334,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMovePlate`
@@ -89,7 +342,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerMoveResource`
@@ -97,7 +350,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerPickUpTips`
@@ -105,7 +358,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerPickUpTips96`
@@ -113,7 +366,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerReturnTips`
@@ -121,7 +374,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerReturnTips96`
@@ -129,7 +382,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `LiquidHandlerStamp`
@@ -137,9 +390,121 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
## 多工作站及小车运行、物料转移
---
### `LiquidHandlerTransfer`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransfer.action
:language: yaml
```
---
### `LiquidHandlerAdd`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAdd.action
:language: yaml
```
---
### `LiquidHandlerIncubateBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
:language: yaml
```
---
### `LiquidHandlerMix`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMix.action
:language: yaml
```
---
### `LiquidHandlerMoveBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveBiomek.action
:language: yaml
```
---
### `LiquidHandlerMoveTo`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveTo.action
:language: yaml
```
---
### `LiquidHandlerOscillateBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
:language: yaml
```
---
### `LiquidHandlerProtocolCreation`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerProtocolCreation.action
:language: yaml
```
---
### `LiquidHandlerRemove`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerRemove.action
:language: yaml
```
---
### `LiquidHandlerSetGroup`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetGroup.action
:language: yaml
```
---
### `LiquidHandlerSetLiquid`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetLiquid.action
:language: yaml
```
---
### `LiquidHandlerSetTipRack`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetTipRack.action
:language: yaml
```
---
### `LiquidHandlerTransferBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferBiomek.action
:language: yaml
```
---
### `LiquidHandlerTransferGroup`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferGroup.action
:language: yaml
```
---
## 多工作站及小车运行、物料转移
### `AGVTransfer`
@@ -147,7 +512,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `WorkStationRun`
@@ -155,12 +520,64 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
----
---
### `ResetHandling`
```{literalinclude} ../../unilabos_msgs/action/ResetHandling.action
:language: yaml
```
---
### `ResourceCreateFromOuter`
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuter.action
:language: yaml
```
---
### `ResourceCreateFromOuterEasy`
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuterEasy.action
:language: yaml
```
---
### `SetPumpPosition`
```{literalinclude} ../../unilabos_msgs/action/SetPumpPosition.action
:language: yaml
```
---
## 固体分配与处理设备操作
### `SolidDispenseAddPowderTube`
```{literalinclude} ../../unilabos_msgs/action/SolidDispenseAddPowderTube.action
:language: yaml
```
---
## 其他设备操作
### `EmptyIn`
```{literalinclude} ../../unilabos_msgs/action/EmptyIn.action
:language: yaml
```
---
## 机械臂、夹爪等机器人设备
Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`
### `FollowJointTrajectory`
```yaml
@@ -228,7 +645,8 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error
```
----
---
### `GripperCommand`
```yaml
@@ -246,42 +664,19 @@ bool reached_goal # True iff the gripper position has reached the commanded setp
```
----
---
### `JointTrajectory`
```yaml
trajectory_msgs/JointTrajectory trajectory
---
---
---
```
----
### `ParallelGripperCommand`
```yaml
# Parallel grippers refer to an end effector where two opposing fingers grasp an object from opposite sides.
sensor_msgs/JointState command
# name: the name(s) of the joint this command is requesting
# position: desired position of each gripper joint (radians or meters)
# velocity: (optional, not used if empty) max velocity of the joint allowed while moving (radians or meters / second)
# effort: (optional, not used if empty) max effort of the joint allowed while moving (Newtons or Newton-meters)
---
sensor_msgs/JointState state # The current gripper state.
# position of each joint (radians or meters)
# optional: velocity of each joint (radians or meters / second)
# optional: effort of each joint (Newtons or Newton-meters)
bool stalled # True if the gripper is exerting max effort and not moving
bool reached_goal # True if the gripper position has reached the commanded setpoint
---
sensor_msgs/JointState state # The current gripper state.
# position of each joint (radians or meters)
# optional: velocity of each joint (radians or meters / second)
# optional: effort of each joint (Newtons or Newton-meters)
```
----
### `PointHead`
```yaml
@@ -291,12 +686,13 @@ string pointing_frame
builtin_interfaces/Duration min_duration
float64 max_velocity
---
---
float64 pointing_angle_error
```
----
---
### `SingleJointPosition`
```yaml
@@ -304,15 +700,16 @@ float64 position
builtin_interfaces/Duration min_duration
float64 max_velocity
---
---
std_msgs/Header header
float64 position
float64 velocity
float64 error
```
----
---
### `AssistedTeleop`
```yaml
@@ -324,10 +721,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback
builtin_interfaces/Duration current_teleop_duration
```
----
---
### `BackUp`
```yaml
@@ -341,10 +738,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 distance_traveled
```
----
---
### `ComputePathThroughPoses`
```yaml
@@ -359,10 +756,10 @@ nav_msgs/Path path
builtin_interfaces/Duration planning_time
---
#feedback definition
```
----
---
### `ComputePathToPose`
```yaml
@@ -377,10 +774,10 @@ nav_msgs/Path path
builtin_interfaces/Duration planning_time
---
#feedback definition
```
----
---
### `DriveOnHeading`
```yaml
@@ -394,10 +791,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 distance_traveled
```
----
---
### `DummyBehavior`
```yaml
@@ -408,10 +805,10 @@ std_msgs/String command
builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
```
----
---
### `FollowPath`
```yaml
@@ -426,10 +823,10 @@ std_msgs/Empty result
#feedback definition
float32 distance_to_goal
float32 speed
```
----
---
### `FollowWaypoints`
```yaml
@@ -441,10 +838,10 @@ int32[] missed_waypoints
---
#feedback definition
uint32 current_waypoint
```
----
---
### `NavigateThroughPoses`
```yaml
@@ -462,10 +859,10 @@ builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
int16 number_of_poses_remaining
```
----
---
### `NavigateToPose`
```yaml
@@ -482,10 +879,10 @@ builtin_interfaces/Duration navigation_time
builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
```
----
---
### `SmoothPath`
```yaml
@@ -501,10 +898,10 @@ builtin_interfaces/Duration smoothing_duration
bool was_completed
---
#feedback definition
```
----
---
### `Spin`
```yaml
@@ -517,10 +914,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 angular_distance_traveled
```
----
---
### `Wait`
```yaml
@@ -532,7 +929,6 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
builtin_interfaces/Duration time_left
```
----
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -32,8 +32,9 @@ developer_guide/device_driver
developer_guide/add_device
developer_guide/add_action
developer_guide/actions
developer_guide/workstation_architecture
developer_guide/add_protocol
developer_guide/add_batteryPLC
developer_guide/materials_tutorial.md
```
## 接口文档

View File

@@ -2,7 +2,6 @@
sphinx>=7.0.0
sphinx-rtd-theme>=2.0.0
myst-parser>=2.0.0
sphinxcontrib-mermaid
# 用于支持Jupyter notebook文档
myst-nb>=1.0.0

54
new_cellconfig.json Normal file
View File

@@ -0,0 +1,54 @@
{
"nodes": [
{
"id": "BatteryStation",
"name": "扣电工作站",
"parent": null,
"children": [
"coin_cell_deck"
],
"type": "device",
"class":"coincellassemblyworkstation_device",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"deck": {
"data": {
"_resource_child_name": "YB_YH_Deck",
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck"
}
},
"debug_mode": true,
"protocol_type": []
}
},
{
"id": "YB_YH_Deck",
"name": "YB_YH_Deck",
"children": [],
"parent": "BatteryStation",
"type": "deck",
"class": "CoincellDeck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "CoincellDeck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
}
],
"links": []
}

98
new_cellconfig3c.json Normal file
View File

@@ -0,0 +1,98 @@
{
"nodes": [
{
"id": "bioyond_cell_workstation",
"name": "配液分液工站",
"parent": null,
"children": [
"YB_Bioyond_Deck"
],
"type": "device",
"class": "bioyond_cell",
"config": {
"deck": {
"data": {
"_resource_child_name": "YB_Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
}
},
"protocol_type": []
},
"data": {}
},
{
"id": "YB_Bioyond_Deck",
"name": "YB_Bioyond_Deck",
"children": [],
"parent": "bioyond_cell_workstation",
"type": "deck",
"class": "BIOYOND_YB_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_YB_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
},
{
"id": "BatteryStation",
"name": "扣电工作站",
"parent": null,
"children": [
"coin_cell_deck"
],
"type": "device",
"class":"coincellassemblyworkstation_device",
"config": {
"deck": {
"data": {
"_resource_child_name": "YB_YH_Deck",
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck"
}
},
"protocol_type": []
},
"position": {
"size": {"height": 1450, "width": 1450, "depth": 2100},
"position": {
"x": -1500,
"y": 0,
"z": 0
}
}
},
{
"id": "YB_YH_Deck",
"name": "YB_YH_Deck",
"children": [],
"parent": "BatteryStation",
"type": "deck",
"class": "CoincellDeck",
"config": {
"type": "CoincellDeck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
}
],
"links": []
}

View File

@@ -24,42 +24,13 @@
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"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"
],
"枪头盒": [
"BIOYOND_PolymerStation_TipBox",
""
],
"反应器": [
"BIOYOND_PolymerStation_Reactor",
""
]
"烧杯": ["YB_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
"试剂瓶": ["YB_1BottleCarrier", ""],
"样品板": ["YB_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"],
"分装板": ["YB_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"],
"样品瓶": ["YB_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"],
"90%分装小瓶": ["YB_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"],
"10%分装小瓶": ["YB_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
}
},
"deck": {
@@ -75,7 +46,8 @@
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"children": [],
"children": [
],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerReactionStation_Deck",
@@ -97,4 +69,4 @@
"data": {}
}
]
}
}

View File

@@ -24,9 +24,9 @@
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"material_type_mappings": {
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
"烧杯": "YB_1FlaskCarrier",
"试剂瓶": "YB_1BottleCarrier",
"样品板": "YB_6VialCarrier"
}
},
"deck": {

View File

@@ -0,0 +1,52 @@
[
{
"id": "3a1d377b-299d-d0f2-ced9-48257f60dfad",
"typeName": "加样头(大)",
"code": "0005-00145",
"barCode": "",
"name": "LiDFOB",
"quantity": 9999.0,
"lockQuantity": 0.0,
"unit": "个",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a19da56-1379-ff7c-1745-07e200b44ce2",
"whid": "3a19da56-1378-613b-29f2-871e1a287aa5",
"whName": "粉末加样头堆栈",
"code": "0005-0001",
"x": 1,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": []
},
{
"id": "3a1d377b-6a81-6a7e-147c-f89f6463656d",
"typeName": "液",
"code": "0006-00141",
"barCode": "",
"name": "EMC",
"quantity": 99999.0,
"lockQuantity": 0.0,
"unit": "g",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a1baa20-a7b1-c665-8b9c-d8099d07d2f6",
"whid": "3a1baa20-a7b0-5c19-8844-5de8924d4e78",
"whName": "4号手套箱内部堆栈",
"code": "0015-0001",
"x": 1,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": []
}
]

View File

@@ -0,0 +1,99 @@
{
"typeId": "3a190c8b-3284-af78-d29f-9a69463ad047",
"code": "",
"barCode": "",
"name": "test",
"unit": "",
"parameters": "{}",
"quantity": "",
"details": [
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)11",
"quantity": "1",
"x": 1,
"y": 1,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)21",
"quantity": "1",
"x": 2,
"y": 1,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)12",
"quantity": "1",
"x": 1,
"y": 2,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)22",
"quantity": "1",
"x": 2,
"y": 2,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)13",
"quantity": "1",
"x": 1,
"y": 3,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)23",
"quantity": "1",
"x": 2,
"y": 3,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)14",
"quantity": "1",
"x": 1,
"y": 4,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)24",
"quantity": "1",
"x": 2,
"y": 4,
"z": 1,
"unit": "",
"parameters": "{}"
}
]
}

148
test/resources/test.json Normal file
View File

@@ -0,0 +1,148 @@
[
{
"id": "3a1d4c14-a9fb-d7dc-9e96-7a3ad6e50219",
"typeName": "配液瓶(小)板",
"code": "0001-00093",
"barCode": "",
"name": "test",
"quantity": 2.0,
"lockQuantity": 0.0,
"unit": "块",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
"whid": "3a19deae-2c79-05a3-9c76-8e6760424841",
"whName": "手动堆栈",
"code": "1",
"x": 1,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": [
{
"id": "3a1d4c14-a9fc-1daa-71fa-146cb1ccb930",
"detailMaterialId": "3a1d4c14-a9fc-4f38-4c48-68486c391c42",
"code": "0001-00093 - 05",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 1,
"y": 3,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-3659-ea61-cd587da9e131",
"detailMaterialId": "3a1d4c14-a9fc-018f-93e5-c49343d37758",
"code": "0001-00093 - 08",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 2,
"y": 4,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-3f94-de83-979d2646e313",
"detailMaterialId": "3a1d4c14-a9fc-9987-c0ef-4b7cbad49e6b",
"code": "0001-00093 - 01",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 1,
"y": 1,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-8c35-6b25-913b11dbaf4e",
"detailMaterialId": "3a1d4c14-a9fc-9a83-865b-0c26ea5e8cc4",
"code": "0001-00093 - 03",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 1,
"y": 2,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-b41f-e968-64953bfddccd",
"detailMaterialId": "3a1d4c14-a9fc-daf7-9d64-e5ec8d3ae0e2",
"code": "0001-00093 - 07",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 1,
"y": 4,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-c20f-c26e-b1bb2cdc3bca",
"detailMaterialId": "3a1d4c14-a9fc-673b-ac83-aaaf71287f1f",
"code": "0001-00093 - 06",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 2,
"y": 3,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-cf21-059c-fde361d82b6f",
"detailMaterialId": "3a1d4c14-a9fc-25b1-e736-6b0d8dac0fae",
"code": "0001-00093 - 02",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 2,
"y": 1,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-d732-2b93-9b2bd2bf581b",
"detailMaterialId": "3a1d4c14-a9fc-7f5d-b6b6-8bcb2e15f320",
"code": "0001-00093 - 04",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 2,
"y": 2,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
}
]
}
]

View File

@@ -1,7 +1,7 @@
import pytest
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
from unilabos.resources.bioyond.bottles import YB_Solid_Vial, YB_Solution_Beaker, YB_Reagent_Bottle
def test_bottle_carrier() -> "BottleCarrier":
@@ -16,9 +16,9 @@ def test_bottle_carrier() -> "BottleCarrier":
print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}")
# 创建瓶子和烧杯
powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01")
solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01")
reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01")
powder_bottle = YB_Solid_Vial("powder_bottle_01")
solution_beaker = YB_Solution_Beaker("solution_beaker_01")
reagent_bottle = YB_Reagent_Bottle("reagent_bottle_01")
print(f"\n创建的物料:")
print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL")

View File

@@ -12,13 +12,13 @@ lab_registry.setup()
type_mapping = {
"烧杯": ("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"),
"烧杯": ("YB_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
"试剂瓶": ("YB_1BottleCarrier", ""),
"样品板": ("YB_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
"分装板": ("YB_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"样品瓶": ("YB_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
"90%分装小瓶": ("YB_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
"10%分装小瓶": ("YB_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
}

View File

@@ -1,3 +1,4 @@
from ast import If
import pytest
import json
import os
@@ -8,18 +9,16 @@ from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.registry.registry import lab_registry
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
from unilabos.resources.bioyond.decks import YB_Deck
lab_registry.setup()
type_mapping = {
"烧杯": ("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"),
"加样头(大)": ("YB_jia_yang_tou_da", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
"": ("YB_1BottleCarrier", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
"配液瓶(小)": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
"配液瓶(小)": ("YB_pei_ye_xiao_Bottler", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
}
@@ -57,12 +56,20 @@ def bioyond_materials_liquidhandling_2() -> list[dict]:
"bioyond_materials_reaction",
"bioyond_materials_liquidhandling_1",
])
def test_resourcetreeset_from_plr(materials_fixture, request) -> list[dict]:
materials = request.getfixturevalue(materials_fixture)
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
def test_resourcetreeset_from_plr() -> list[dict]:
# 直接加载 bioyond_materials_reaction.json 文件
current_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(current_dir, "test.json")
with open(json_path, "r", encoding="utf-8") as f:
materials = json.load(f)
deck = YB_Deck("test_deck")
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
print(deck.summary())
print(output)
# print(deck.summary())
r = ResourceTreeSet.from_plr_resources([deck])
print(r.dump())
# json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)
if __name__ == "__main__":
test_resourcetreeset_from_plr()

View File

@@ -13,7 +13,7 @@ def start_backend(
graph=None,
controllers_config: dict = {},
bridges=[],
without_host: bool = False,
is_slave: bool = False,
visual: str = "None",
resources_mesh_config: dict = {},
**kwargs,
@@ -32,7 +32,7 @@ def start_backend(
raise ValueError(f"Unsupported backend: {backend}")
backend_thread = threading.Thread(
target=main if not without_host else slave,
target=main if not is_slave else slave,
args=(
devices_config,
resources_config,

View File

@@ -375,22 +375,23 @@ def main():
args_dict["bridges"] = []
# 获取通信客户端仅支持WebSocket
comm_client = get_communication_client()
if "websocket" in args_dict["app_bridges"]:
args_dict["bridges"].append(comm_client)
if "fastapi" in args_dict["app_bridges"]:
args_dict["bridges"].append(http_client)
if "websocket" in args_dict["app_bridges"]:
# 获取通信客户端仅支持WebSocket
if BasicConfig.is_host_mode:
comm_client = get_communication_client()
if "websocket" in args_dict["app_bridges"]:
args_dict["bridges"].append(comm_client)
def _exit(signum, frame):
comm_client.stop()
sys.exit(0)
def _exit(signum, frame):
comm_client.stop()
sys.exit(0)
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
comm_client.start()
else:
print_status("SlaveMode跳过Websocket连接")
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
comm_client.start()
args_dict["resources_mesh_config"] = {}
args_dict["resources_edge_config"] = resource_edge_info
# web visiualize 2D

View File

@@ -1,11 +1,12 @@
import json
import time
from typing import Optional, Tuple, Dict, Any
from unilabos.utils.log import logger
from unilabos.utils.type_check import TypeEncoder
def register_devices_and_resources(lab_registry):
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
"""
注册设备和资源到服务器仅支持HTTP
"""
@@ -28,6 +29,8 @@ def register_devices_and_resources(lab_registry):
resources_to_register[resource_info["id"]] = resource_info
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
if gather_only:
return devices_to_register, resources_to_register
# 注册设备
if devices_to_register:
try:

View File

@@ -421,7 +421,7 @@ class MessageProcessor:
ssl_context = ssl_module.create_default_context()
ws_logger = logging.getLogger("websockets.client")
ws_logger.setLevel(logging.INFO)
# 日志级别已在 unilabos.utils.log 中统一配置为 WARNING
async with websockets.connect(
self.websocket_url,
@@ -1197,7 +1197,7 @@ class WebSocketClient(BaseCommunicationClient):
},
}
self.message_processor.send_message(message)
logger.debug(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
def publish_job_status(
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None

View File

@@ -0,0 +1,296 @@
# -*- coding: utf-8 -*-
import serial
import time
import csv
import threading
import os
from collections import deque
from typing import Dict, Any, Optional
from pylabrobot.resources import Deck
from unilabos.devices.workstation.workstation_base import WorkstationBase
class ElectrolysisWaterPlatform(WorkstationBase):
"""
电解水平台工作站
基于 WorkstationBase 的电解水实验平台,支持串口通信和数据采集
"""
def __init__(
self,
deck: Deck,
port: str = "COM10",
baudrate: int = 115200,
csv_path: Optional[str] = None,
timeout: float = 0.2,
**kwargs
):
super().__init__(deck, **kwargs)
# ========== 配置 ==========
self.port = port
self.baudrate = baudrate
# 如果没有指定路径,默认保存在代码文件所在目录
if csv_path is None:
current_dir = os.path.dirname(os.path.abspath(__file__))
self.csv_path = os.path.join(current_dir, "stm32_data.csv")
else:
self.csv_path = csv_path
self.ser_timeout = timeout
self.chunk_read = 128
# 串口对象
self.ser: Optional[serial.Serial] = None
self.stop_flag = False
# 线程对象
self.rx_thread: Optional[threading.Thread] = None
self.tx_thread: Optional[threading.Thread] = None
# ==== 接收(下位机->上位机):固定 1+13+1 = 15 字节 ====
self.RX_HEAD = 0x3E
self.RX_TAIL = 0x3E
self.RX_FRAME_LEN = 1 + 13 + 1 # 15
# ==== 发送(上位机->下位机):固定 1+9+1 = 11 字节 ====
self.TX_HEAD = 0x3E
self.TX_TAIL = 0xE3 # 协议图中标注 E3 作为帧尾
self.TX_FRAME_LEN = 1 + 9 + 1 # 11
def open_serial(self, port: Optional[str] = None, baudrate: Optional[int] = None, timeout: Optional[float] = None) -> Optional[serial.Serial]:
"""打开串口"""
port = port or self.port
baudrate = baudrate or self.baudrate
timeout = timeout or self.ser_timeout
try:
ser = serial.Serial(port, baudrate, timeout=timeout)
print(f"[OK] 串口 {port} 已打开,波特率 {baudrate}")
ser.reset_input_buffer()
ser.reset_output_buffer()
self.ser = ser
return ser
except serial.SerialException as e:
print(f"[ERR] 无法打开串口 {port}: {e}")
return None
def close_serial(self):
"""关闭串口"""
if self.ser and self.ser.is_open:
self.ser.close()
print("[INFO] 串口已关闭")
@staticmethod
def u16_be(h: int, l: int) -> int:
"""将两个字节组合成16位无符号整数大端序"""
return ((h & 0xFF) << 8) | (l & 0xFF)
@staticmethod
def split_u16_be(val: int) -> tuple:
"""返回 (高字节, 低字节),输入会夹到 0..65535"""
v = int(max(0, min(65535, int(val))))
return (v >> 8) & 0xFF, v & 0xFF
# ================== 接收固定15字节 ==================
def parse_rx_payload(self, dat13: bytes) -> Optional[Dict[str, Any]]:
"""解析 13 字节数据区(下位机发送到上位机)"""
if len(dat13) != 13:
return None
current_mA = self.u16_be(dat13[0], dat13[1])
voltage_mV = self.u16_be(dat13[2], dat13[3])
temperature_raw = self.u16_be(dat13[4], dat13[5])
tds_ppm = self.u16_be(dat13[6], dat13[7])
gas_sccm = self.u16_be(dat13[8], dat13[9])
liquid_mL = self.u16_be(dat13[10], dat13[11])
ph_raw = dat13[12] & 0xFF
return {
"Current_mA": current_mA,
"Voltage_mV": voltage_mV,
"Temperature_C": round(temperature_raw / 100.0, 2),
"TDS_ppm": tds_ppm,
"GasFlow_sccm": gas_sccm,
"LiquidFlow_mL": liquid_mL,
"pH": round(ph_raw / 10.0, 2)
}
def try_parse_rx_frame(self, frame15: bytes) -> Optional[Dict[str, Any]]:
"""尝试解析接收帧"""
if len(frame15) != self.RX_FRAME_LEN:
return None
if frame15[0] != self.RX_HEAD or frame15[-1] != self.RX_TAIL:
return None
return self.parse_rx_payload(frame15[1:-1])
def rx_thread_fn(self):
"""接收线程函数"""
headers = ["Timestamp", "Current_mA", "Voltage_mV",
"Temperature_C", "TDS_ppm", "GasFlow_sccm", "LiquidFlow_mL", "pH"]
new_file = not os.path.exists(self.csv_path)
f = open(self.csv_path, mode='a', newline='', encoding='utf-8')
writer = csv.writer(f)
if new_file:
writer.writerow(headers)
f.flush()
buf = deque(maxlen=8192)
print(f"[RX] 开始接收(帧长 {self.RX_FRAME_LEN} 字节);写入:{self.csv_path}")
try:
while not self.stop_flag and self.ser and self.ser.is_open:
chunk = self.ser.read(self.chunk_read)
if chunk:
buf.extend(chunk)
while True:
# 找帧头
try:
start = next(i for i, b in enumerate(buf) if b == self.RX_HEAD)
except StopIteration:
buf.clear()
break
if start > 0:
for _ in range(start):
buf.popleft()
if len(buf) < self.RX_FRAME_LEN:
break
candidate = bytes([buf[i] for i in range(self.RX_FRAME_LEN)])
if candidate[-1] == self.RX_TAIL:
parsed = self.try_parse_rx_frame(candidate)
for _ in range(self.RX_FRAME_LEN):
buf.popleft()
if parsed:
ts = time.strftime("%Y-%m-%d %H:%M:%S")
row = [ts,
parsed["Current_mA"], parsed["Voltage_mV"],
parsed["Temperature_C"], parsed["TDS_ppm"],
parsed["GasFlow_sccm"], parsed["LiquidFlow_mL"],
parsed["pH"]]
writer.writerow(row)
f.flush()
# 若不想打印可注释下一行
# print(f"[{ts}] I={parsed['Current_mA']} mA, V={parsed['Voltage_mV']} mV, "
# f"T={parsed['Temperature_C']} °C, TDS={parsed['TDS_ppm']}, "
# f"Gas={parsed['GasFlow_sccm']} sccm, Liq={parsed['LiquidFlow_mL']} mL, pH={parsed['pH']}")
else:
# 头不变尾不对丢1字节继续对齐
buf.popleft()
else:
time.sleep(0.01)
finally:
f.close()
print("[RX] 接收线程退出CSV 已关闭")
# ================== 发送固定11字节 ==================
def build_tx_frame(self, mode: int, current_ma: int, voltage_mv: int, temp_c: float, ki: float, pump_percent: float) -> bytes:
"""
发送帧HEAD + [mode, I_hi, I_lo, V_hi, V_lo, T_hi, T_lo, Ki_byte, Pump_byte] + TAIL
- mode: 0=恒压, 1=恒流
- current_ma: mA (0..65535)
- voltage_mv: mV (0..65535)
- temp_c: ℃,将 *100 后拆分为高/低字节
- ki: 0.0..20.0 -> byte = round(ki * 10) 夹到 0..200
- pump_percent: 0..100 -> byte = round(pump * 2) 夹到 0..200
"""
mode_b = 1 if int(mode) == 1 else 0
i_hi, i_lo = self.split_u16_be(current_ma)
v_hi, v_lo = self.split_u16_be(voltage_mv)
t100 = int(round(float(temp_c) * 100.0))
t_hi, t_lo = self.split_u16_be(t100)
ki_b = int(max(0, min(200, round(float(ki) * 10))))
pump_b = int(max(0, min(200, round(float(pump_percent) * 2))))
return bytes((
self.TX_HEAD,
mode_b,
i_hi, i_lo,
v_hi, v_lo,
t_hi, t_lo,
ki_b,
pump_b,
self.TX_TAIL
))
def tx_thread_fn(self):
"""
发送线程函数
用户输入 6 个用逗号分隔的数值:
mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent
例如: 0,1000,500,0,0,50
"""
print("\n输入 6 个值(用英文逗号分隔),顺序为:")
print("mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent")
print("示例恒压0,500,1000,25,0,100 stop 结束)\n")
print("示例恒流1,1000,500,25,0,100 stop 结束)\n")
print("示例恒流1,2000,500,25,0,100 stop 结束)\n")
# 1,2000,500,25,0,100
while not self.stop_flag and self.ser and self.ser.is_open:
try:
line = input(">>> ").strip()
except EOFError:
self.stop_flag = True
break
if not line:
continue
if line.lower() == "stop":
self.stop_flag = True
print("[SYS] 停止程序")
break
try:
parts = [p.strip() for p in line.split(",")]
if len(parts) != 6:
raise ValueError("需要 6 个逗号分隔的数值")
mode = int(parts[0])
i_ma = int(float(parts[1]))
v_mv = int(float(parts[2]))
t_c = float(parts[3])
ki = float(parts[4])
pump = float(parts[5])
frame = self.build_tx_frame(mode, i_ma, v_mv, t_c, ki, pump)
self.ser.write(frame)
print("[TX]", " ".join(f"{b:02X}" for b in frame))
except Exception as e:
print("[TX] 输入/打包失败:", e)
print("格式mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent")
continue
def start(self):
"""启动电解水平台"""
self.ser = self.open_serial()
if self.ser:
try:
self.rx_thread = threading.Thread(target=self.rx_thread_fn, daemon=True)
self.tx_thread = threading.Thread(target=self.tx_thread_fn, daemon=True)
self.rx_thread.start()
self.tx_thread.start()
print("[INFO] 电解水平台已启动")
self.tx_thread.join() # 等待用户输入线程结束(输入 stop
finally:
self.close_serial()
def stop(self):
"""停止电解水平台"""
self.stop_flag = True
if self.rx_thread and self.rx_thread.is_alive():
self.rx_thread.join(timeout=2.0)
if self.tx_thread and self.tx_thread.is_alive():
self.tx_thread.join(timeout=2.0)
self.close_serial()
print("[INFO] 电解水平台已停止")
# ================== 主入口 ==================
if __name__ == "__main__":
# 创建一个简单的 Deck 用于测试
from pylabrobot.resources import Deck
deck = Deck()
platform = ElectrolysisWaterPlatform(deck)
platform.start()

View File

@@ -1,11 +1,11 @@
from __future__ import annotations
import re
import traceback
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast
from collections import Counter
import asyncio
import time
import pprint as pp
import traceback
from collections import Counter
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast
from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, LiquidHandlerChatterboxBackend, Strictness
from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod
from pylabrobot.liquid_handling.standard import GripDirection

View File

@@ -1,20 +0,0 @@
{
"nodes": [
{
"id": "opsky_ATR30007",
"name": "opsky_ATR30007",
"parent": null,
"type": "device",
"class": "opsky_ATR30007",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {},
"data": {},
"children": []
}
],
"links": []
}

View File

@@ -1,71 +0,0 @@
import socket
import time
import csv
from datetime import datetime
import threading
csv_lock = threading.Lock() # 防止多线程写CSV冲突
def scan_once(ip="192.168.1.50", port_in=2001, port_out=2002,
csv_file="scan_results.csv", timeout=5, retries=3):
"""
改进版扫码函数:
- 自动重试
- 全程超时保护
- 更安全的socket关闭
- 文件写入加锁
"""
def save_result(qrcode):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with csv_lock:
with open(csv_file, mode="a", newline="") as f:
writer = csv.writer(f)
writer.writerow([timestamp, qrcode])
print(f"✅ 已保存结果: {timestamp}, {qrcode}")
result = None
for attempt in range(1, retries + 1):
print(f"\n🟡 扫码尝试 {attempt}/{retries} ...")
try:
# -------- Step 1: 触发拍照 --------
with socket.create_connection((ip, port_in), timeout=2) as client_in:
cmd = "start"
client_in.sendall(cmd.encode("ascii")) #把字符串转为byte字节流规则是ascii码
print(f"→ 已发送触发指令: {cmd}")
# -------- Step 2: 等待识别结果 --------
with socket.create_connection((ip, port_out), timeout=timeout) as client_out:
print(f" 已连接相机输出端口 {port_out},等待结果...")
# recv最多阻塞timeout秒
client_out.settimeout(timeout)
data = client_out.recv(2048).decode("ascii", errors="ignore").strip() #结果输出为ascii字符串遇到无法解析的字节则忽略
# .strip():去掉字符串头尾的空白字符(包括 \n, \r, 空格等),便于后续判断是否为空或写入 CSV。
if data:
print(f"📷 识别结果: {data}")
save_result(data) #调用 save_result(data) 把时间戳 + 识别字符串写入 CSV线程安全
result = data #把局部变量 result 设为 data用于函数返回值
break #如果读取成功跳出重试循环for attempt in ...),不再进行后续重试。
else:
print("⚠️ 相机返回空数据,重试中...")
except socket.timeout:
print("⏰ 超时未收到识别结果,重试中...")
except ConnectionRefusedError:
print("❌ 无法连接到扫码器端口,请检查设备是否在线。")
except OSError as e:
print(f"⚠️ 网络错误: {e}")
except Exception as e:
print(f"❌ 未知异常: {e}")
time.sleep(0.5) # 两次扫描之间稍作延时
# -------- Step 3: 返回最终结果 --------
if result:
print(f"✅ 扫码成功:{result}")
else:
print("❌ 多次尝试后仍未获取二维码结果")
return result

View File

@@ -1,398 +0,0 @@
# opsky_atr30007.py
import logging
import time as time_mod
import csv
from datetime import datetime
from typing import Optional, Dict, Any
# 兼容 pymodbus 在不同版本中的位置与 API
try:
from pymodbus.client import ModbusTcpClient
except Exception:
ModbusTcpClient = None
# 导入 run_raman_test假定与本文件同目录
# 如果你的项目是包结构且原先使用相对导入,请改回 `from .raman_module import run_raman_test`
try:
from .raman_module import run_raman_test
except Exception:
# 延迟导入失败不会阻止主流程(在 run 时会再尝试)
run_raman_test = None
logger = logging.getLogger("opsky")
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", "%y-%m-%d %H:%M:%S")
ch.setFormatter(formatter)
logger.addHandler(ch)
class opsky_ATR30007:
"""
封装 UniLabOS 设备动作逻辑,兼容 pymodbus 2.x / 3.x。
放在独立文件中opsky_atr30007.py
"""
def __init__(
self,
plc_ip: str = "192.168.1.88",
plc_port: int = 502,
robot_ip: str = "192.168.1.200",
robot_port: int = 502,
scan_csv_file: str = "scan_results.csv",
):
self.plc_ip = plc_ip
self.plc_port = plc_port
self.robot_ip = robot_ip
self.robot_port = robot_port
self.scan_csv_file = scan_csv_file
# ----------------- 参数字符串转换 helpers -----------------
@staticmethod
def _str_to_int(s, default):
try:
return int(float(str(s).strip()))
except Exception:
return int(default)
@staticmethod
def _str_to_float(s, default):
try:
return float(str(s).strip())
except Exception:
return float(default)
@staticmethod
def _str_to_bool(s, default):
try:
v = str(s).strip().lower()
if v in ("true", "1", "yes", "y", "t"):
return True
if v in ("false", "0", "no", "n", "f"):
return False
return default
except Exception:
return default
# ----------------- Modbus / 安全读写 -----------------
@staticmethod
def _adapt_req_kwargs_for_read(func_name: str, args: tuple, kwargs: dict):
# 如果调用方传的是 (address, count) positional在新版接口可能是 address=..., count=...
if len(args) == 2 and func_name.startswith("read_"):
address, count = args
args = ()
kwargs.setdefault("address", address)
kwargs.setdefault("count", count)
return args, kwargs
@staticmethod
def _adapt_req_kwargs_for_write(func_name: str, args: tuple, kwargs: dict):
if len(args) == 2 and func_name.startswith("write_"):
address, value = args
args = ()
kwargs.setdefault("address", address)
kwargs.setdefault("value", value)
return args, kwargs
def ensure_connected(self, client, name, ip, port):
"""确保连接存在,失败则尝试重连并返回新的 client 或 None"""
if client is None:
return None
try:
# 不同 pymodbus 版本可能有不同方法检测 socket
is_open = False
try:
is_open = bool(client.is_socket_open())
except Exception:
# fallback: try to read nothing or attempt connection test
try:
# 轻试一次
is_open = client.connected if hasattr(client, "connected") else False
except Exception:
is_open = False
if not is_open:
logger.warning("%s 掉线,尝试重连...", name)
try:
client.close()
except Exception:
pass
time_mod.sleep(0.5)
if ModbusTcpClient:
new_client = ModbusTcpClient(ip, port=port)
try:
if new_client.connect():
logger.info("%s 重新连接成功 (%s:%s)", name, ip, port)
return new_client
except Exception:
pass
logger.warning("%s 重连失败", name)
time_mod.sleep(1)
return None
return client
except Exception as e:
logger.exception("%s 连接检查异常: %s", name, e)
return None
def safe_read(self, client, name, func, *args, retries=3, delay=0.3, **kwargs):
"""兼容 pymodbus 2.x/3.x 的读函数,返回 response 或 None"""
if client is None:
return None
for attempt in range(1, retries + 1):
try:
# adapt args/kwargs for different API styles
args, kwargs = self._adapt_req_kwargs_for_read(func.__name__, args, kwargs)
# unit->slave compatibility
if "unit" in kwargs:
kwargs["slave"] = kwargs.pop("unit")
res = func(*args, **kwargs)
# pymodbus Response 在不同版本表现不同,尽量检测错误
if res is None:
raise RuntimeError("返回 None")
if hasattr(res, "isError") and res.isError():
raise RuntimeError("Modbus 返回 isError()")
return res
except Exception as e:
logger.warning("%s 读异常 (尝试 %d/%d): %s", name, attempt, retries, e)
time_mod.sleep(delay)
logger.error("%s 连续读取失败 %d", name, retries)
return None
def safe_write(self, client, name, func, *args, retries=3, delay=0.3, **kwargs):
"""兼容 pymodbus 2.x/3.x 的写函数,返回 True/False"""
if client is None:
return False
for attempt in range(1, retries + 1):
try:
args, kwargs = self._adapt_req_kwargs_for_write(func.__name__, args, kwargs)
if "unit" in kwargs:
kwargs["slave"] = kwargs.pop("unit")
res = func(*args, **kwargs)
if res is None:
raise RuntimeError("返回 None")
if hasattr(res, "isError") and res.isError():
raise RuntimeError("Modbus 返回 isError()")
return True
except Exception as e:
logger.warning("%s 写异常 (尝试 %d/%d): %s", name, attempt, retries, e)
time_mod.sleep(delay)
logger.error("%s 连续写入失败 %d", name, retries)
return False
def wait_with_quit_check(self, robot, seconds, addr_quit=270):
"""等待指定时间,同时每 0.2s 检查 R270 是否为 1立即退出"""
if robot is None:
time_mod.sleep(seconds)
return False
checks = max(1, int(seconds / 0.2))
for _ in range(checks):
rr = self.safe_read(robot, "机器人", robot.read_holding_registers, address=addr_quit, count=1)
if rr and getattr(rr, "registers", [None])[0] == 1:
logger.info("检测到 R270=1立即退出等待")
return True
time_mod.sleep(0.2)
return False
# ----------------- 主流程 run_once -----------------
def run_once(
self,
integration_time: str = "5000",
laser_power: str = "200",
save_csv: str = "true",
save_plot: str = "true",
normalize: str = "true",
norm_max: str = "1.0",
**_: Any,
) -> Dict[str, Any]:
result: Dict[str, Any] = {"success": False, "event": "none", "details": {}}
integration_time_v = self._str_to_int(integration_time, 5000)
laser_power_v = self._str_to_int(laser_power, 200)
save_csv_v = self._str_to_bool(save_csv, True)
save_plot_v = self._str_to_bool(save_plot, True)
normalize_v = self._str_to_bool(normalize, True)
norm_max_v = None if norm_max in (None, "", "none", "null") else self._str_to_float(norm_max, 1.0)
if ModbusTcpClient is None:
result["details"]["error"] = "未安装 pymodbus无法执行连接"
logger.error(result["details"]["error"])
return result
# 建立连接
plc = ModbusTcpClient(self.plc_ip, port=self.plc_port)
robot = ModbusTcpClient(self.robot_ip, port=self.robot_port)
try:
if not plc.connect():
result["details"]["error"] = "无法连接 PLC"
logger.error(result["details"]["error"])
return result
if not robot.connect():
plc.close()
result["details"]["error"] = "无法连接 机器人"
logger.error(result["details"]["error"])
return result
logger.info("✅ PLC 与 机器人连接成功")
time_mod.sleep(0.2)
# 伺服使能 (coil 写示例)
if self.safe_write(plc, "PLC", plc.write_coil, 10, True):
logger.info("✅ 伺服使能成功 (M10=True)")
else:
logger.warning("⚠️ 伺服使能失败")
# 初始化 CSV 文件
try:
with open(self.scan_csv_file, "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerow(["Bottle_No", "Scan_Result", "Time"])
except Exception as e:
logger.warning("⚠️ 初始化CSV失败: %s", e)
bottle_count = 0
logger.info("🟢 等待机器人触发信号... (R260=1扫码 / R256=1拉曼 / R270=1退出)")
# 主循环:仅响应事件(每次循环后短暂 sleep
while True:
plc = self.ensure_connected(plc, "PLC", self.plc_ip, self.plc_port) or plc
robot = self.ensure_connected(robot, "机器人", self.robot_ip, self.robot_port) or robot
# 检查退出寄存器
quit_signal = self.safe_read(robot, "机器人", robot.read_holding_registers, 270, 1)
if quit_signal and getattr(quit_signal, "registers", [None])[0] == 1:
logger.info("🟥 检测到 R270=1准备退出...")
result["event"] = "quit"
result["success"] = True
break
# 读取关键寄存器256..260
rr = self.safe_read(robot, "机器人", robot.read_holding_registers, 256, 5)
if not rr or not hasattr(rr, "registers"):
time_mod.sleep(0.3)
continue
r256, r257, r258, r259, r260 = (rr.registers + [0, 0, 0, 0, 0])[:5]
# ---------- 扫码逻辑 ----------
if r260 == 1:
bottle_count += 1
logger.info("📸 第 %d 瓶触发扫码 (R260=1)", bottle_count)
try:
# 调用外部扫码函数(用户实现)
from .dmqfengzhuang import scan_once as scan_once_local
scan_result = scan_once_local(ip="192.168.1.50", port_in=2001, port_out=2002)
if scan_result:
logger.info("✅ 扫码成功: %s", scan_result)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(self.scan_csv_file, "a", newline="", encoding="utf-8") as f:
csv.writer(f).writerow([bottle_count, scan_result, timestamp])
else:
logger.warning("⚠️ 扫码失败或无返回")
except Exception as e:
logger.exception("❌ 扫码异常: %s", e)
# 写 R260->0, R261->1
self.safe_write(robot, "机器人", robot.write_register, 260, 0)
time_mod.sleep(0.15)
self.safe_write(robot, "机器人", robot.write_register, 261, 1)
logger.info("➡️ 扫码完成 (R260→0, R261→1)")
result["event"] = "scan"
result["success"] = True
# ---------- 拉曼逻辑 ----------
if r256 == 1:
logger.info("⚙️ 检测到 R256=1放瓶完成")
# PLC 电机右转指令
self.safe_write(plc, "PLC", plc.write_register, 1199, 1)
self.safe_write(plc, "PLC", plc.write_register, 1200, 1)
logger.info("➡️ 电机右转中...")
if self.wait_with_quit_check(robot, 3):
result["event"] = "quit"
break
self.safe_write(plc, "PLC", plc.write_register, 1199, 0)
logger.info("✅ 电机右转完成")
# 调用拉曼测试(尽量捕获异常并记录)
logger.info("🧪 开始拉曼测试...")
try:
# 尝试使用模块导入好的 run_raman_test否则再动态导入
rr_func = run_raman_test
if rr_func is None:
from raman_module import run_raman_test as rr_func
success, file_prefix, df = rr_func(
integration_time=integration_time_v,
laser_power=laser_power_v,
save_csv=save_csv_v,
save_plot=save_plot_v,
normalize=normalize_v,
norm_max=norm_max_v,
)
if success:
logger.info("✅ 拉曼测试完成: %s", file_prefix)
result["event"] = "raman"
result["success"] = True
else:
logger.warning("⚠️ 拉曼测试失败")
except Exception as e:
logger.exception("❌ 拉曼模块异常: %s", e)
# 电机左转回位
self.safe_write(plc, "PLC", plc.write_register, address=1299, value=1)
self.safe_write(plc, "PLC", plc.write_register, address=1300, value=1)
logger.info("⬅️ 电机左转中...")
if self.wait_with_quit_check(robot, 3):
result["event"] = "quit"
break
self.safe_write(plc, "PLC", plc.write_register, address=1299, value=0)
logger.info("✅ 电机左转完成")
# 通知机器人拉曼完成 R257=1
self.safe_write(robot, "机器人", robot.write_register, address=257, value=1)
logger.info("✅ 已写入 R257=1拉曼完成")
# 延迟后清零 R256
logger.info("⏳ 延迟4秒后清零 R256")
if self.wait_with_quit_check(robot, 4):
result["event"] = "quit"
break
self.safe_write(robot, "机器人", robot.write_register, address=256, value=0)
logger.info("✅ 已清零 R256")
# 等待机器人清 R257
logger.info("等待 R257 清零中...")
while True:
rr2 = self.safe_read(robot, "机器人", robot.read_holding_registers, address=257, count=1)
if rr2 and getattr(rr2, "registers", [None])[0] == 0:
logger.info("✅ 检测到 R257=0准备下一循环")
break
if self.wait_with_quit_check(robot, 1):
result["event"] = "quit"
break
time_mod.sleep(0.2)
time_mod.sleep(0.25)
finally:
logger.info("🧹 开始清理...")
try:
self.safe_write(plc, "PLC", plc.write_coil, address=10, value=False)
except Exception:
pass
for addr in [256, 257, 260, 261, 270]:
try:
self.safe_write(robot, "机器人", robot.write_register, address=addr, value=0)
except Exception:
pass
try:
if plc:
plc.close()
except Exception:
pass
try:
if robot:
robot.close()
except Exception:
pass
logger.info("🔚 已关闭所有连接")
return result

View File

@@ -1,180 +0,0 @@
# raman_module.py
import os
import time as time_mod
import numpy as np
import pandas as pd
# clr / ATRWrapper 依赖:在真实环境中使用 Windows + .NET wrapper
# 本模块对缺少 clr 或 Wrapper 的情况提供“仿真”回退,方便离线/调试运行。
try:
import clr
has_clr = True
except Exception:
clr = None
has_clr = False
# 本函数返回 (success: bool, file_prefix: str|None, df: pandas.DataFrame|None)
def run_raman_test(integration_time=5000, laser_power=200,
save_csv=True, save_plot=True,
normalize=False, norm_max=None,
max_wavenum=1300):
"""
拉曼测试流程(更稳健的实现):
- 若能加载 ATRWrapper 则使用之
- 否则生成模拟光谱(方便调试)
返回 (success, file_prefix, df)
"""
timestamp = time_mod.strftime("%Y%m%d_%H%M%S")
file_prefix = f"raman_{timestamp}"
wrapper = None
used_real_device = False
try:
if has_clr:
try:
# 请根据你的 DLL 路径调整
dll_path = r"D:\Raman\Raman_RS\ATRWrapper\ATRWrapper.dll"
if os.path.exists(dll_path):
clr.AddReference(dll_path)
else:
# 试图直接 AddReference 名称(若已在 PATH
try:
clr.AddReference("ATRWrapper")
except Exception:
pass
from Optosky.Wrapper import ATRWrapper # May raise
wrapper = ATRWrapper()
used_real_device = True
except Exception as e:
# 无法加载真实 wrapper -> fallback
print("⚠️ 未能加载 ATRWrapper使用模拟数据。详细:", e)
wrapper = None
if wrapper is None:
# 生成模拟光谱(方便调试)
# 模拟波数轴 50..1300
WaveNum = np.linspace(50, max_wavenum, 1024)
# 合成几条高斯峰 + 噪声
def gauss(x, mu, sig, A):
return A * np.exp(-0.5 * ((x - mu) / sig) ** 2)
Spect_data = (gauss(WaveNum, 200, 8, 1000) +
gauss(WaveNum, 520, 12, 600) +
gauss(WaveNum, 810, 20, 400) +
50 * np.random.normal(scale=1.0, size=WaveNum.shape))
Spect_bLC = Spect_data - np.min(Spect_data) * 0.05 # 简单 baseline
Spect_smooth = np.convolve(Spect_bLC, np.ones(3) / 3, mode="same")
df = pd.DataFrame({
"WaveNum": WaveNum,
"Raw_Spect": Spect_data,
"BaseLineCorrected": Spect_bLC,
"Smooth_Spect": Spect_smooth
})
success = True
file_prefix = f"raman_sim_{timestamp}"
# 保存 CSV / 绘图 等同真实设备
else:
# 使用真实设备 API根据你提供的 wrapper 调用)
On_flag = wrapper.OpenDevice()
print("通讯连接状态:", On_flag)
if not On_flag:
wrapper.CloseDevice()
return False, None, None
wrapper.SetIntegrationTime(int(integration_time))
wrapper.SetLdPower(int(laser_power), 1)
# 可能的冷却设置(如果 wrapper 支持)
try:
wrapper.SetCool(-5)
except Exception:
pass
Spect = wrapper.AcquireSpectrum()
Spect_data = np.array(Spect.get_Data())
if not Spect.get_Success():
print("光谱采集失败")
try:
wrapper.CloseDevice()
except Exception:
pass
return False, None, None
WaveNum = np.array(wrapper.GetWaveNum())
Spect_bLC = np.array(wrapper.BaseLineCorrect(Spect_data))
Spect_smooth = np.array(wrapper.SmoothBoxcar(Spect_bLC, 3))
df = pd.DataFrame({
"WaveNum": WaveNum,
"Raw_Spect": Spect_data,
"BaseLineCorrected": Spect_bLC,
"Smooth_Spect": Spect_smooth
})
wrapper.CloseDevice()
success = True
# 如果需要限定波数范围
mask = df["WaveNum"] <= max_wavenum
df = df[mask].reset_index(drop=True)
# 可选归一化
if normalize:
arr = df["Smooth_Spect"].values
mn, mx = arr.min(), arr.max()
if mx == mn:
df["Smooth_Spect"] = 0.0
else:
scale = 1.0 if norm_max is None else float(norm_max)
df["Smooth_Spect"] = (arr - mn) / (mx - mn) * scale
# 同时处理其它列(可选)
arr_raw = df["Raw_Spect"].values
mn_r, mx_r = arr_raw.min(), arr_raw.max()
if mx_r == mn_r:
df["Raw_Spect"] = 0.0
else:
scale = 1.0 if norm_max is None else float(norm_max)
df["Raw_Spect"] = (arr_raw - mn_r) / (mx_r - mn_r) * scale
# 保存 CSV
if save_csv:
csv_filename = f"{file_prefix}.csv"
df.to_csv(csv_filename, index=False)
print("✅ CSV 文件已生成:", csv_filename)
# 绘图(使用 matplotlib注意不要启用 GUI 后台
if save_plot:
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 5))
plt.plot(df["WaveNum"], df["Raw_Spect"], linestyle='-', alpha=0.6, label="原始")
plt.plot(df["WaveNum"], df["BaseLineCorrected"], linestyle='--', alpha=0.8, label="基线校正")
plt.plot(df["WaveNum"], df["Smooth_Spect"], linewidth=1.2, label="平滑")
plt.xlabel("WaveNum (cm^-1)")
plt.ylabel("Intensity (a.u.)")
plt.title(f"Raman {file_prefix}")
plt.grid(True)
plt.legend()
plt.tight_layout()
plot_filename = f"{file_prefix}.png"
plt.savefig(plot_filename, dpi=300, bbox_inches="tight")
plt.close()
# 小短暂等待以确保文件系统刷新
time_mod.sleep(0.2)
print("✅ 图像已生成:", plot_filename)
except Exception as e:
print("⚠️ 绘图失败:", e)
return success, file_prefix, df
except Exception as e:
print("拉曼测试异常:", e)
try:
if wrapper is not None:
try:
wrapper.CloseDevice()
except Exception:
pass
except Exception:
pass
return False, None, None

View File

@@ -1,209 +0,0 @@
import time
import csv
from datetime import datetime
from pymodbus.client import ModbusTcpClient
from dmqfengzhuang import scan_once
from raman_module import run_raman_test
# =================== 配置 ===================
PLC_IP = "192.168.1.88"
PLC_PORT = 502
ROBOT_IP = "192.168.1.200"
ROBOT_PORT = 502
SCAN_CSV_FILE = "scan_results.csv"
# =================== 通用函数 ===================
def ensure_connected(client, name, ip, port):
if not client.is_socket_open():
print(f"{name} 掉线,正在重连...")
client.close()
time.sleep(1)
new_client = ModbusTcpClient(ip, port=port)
if new_client.connect():
print(f"{name} 重新连接成功 ({ip}:{port})")
return new_client
else:
print(f"{name} 重连失败,稍后重试...")
time.sleep(3)
return None
return client
def safe_read(client, name, func, *args, retries=3, delay=0.3, **kwargs):
for _ in range(retries):
try:
res = func(*args, **kwargs)
if res and not (hasattr(res, "isError") and res.isError()):
return res
except Exception as e:
print(f"{name} 读异常: {e}")
time.sleep(delay)
print(f"{name} 连续读取失败 {retries}")
return None
def safe_write(client, name, func, *args, retries=3, delay=0.3, **kwargs):
for _ in range(retries):
try:
res = func(*args, **kwargs)
if res and not (hasattr(res, "isError") and res.isError()):
return True
except Exception as e:
print(f"{name} 写异常: {e}")
time.sleep(delay)
print(f"{name} 连续写入失败 {retries}")
return False
def wait_with_quit_check(robot, seconds, addr_quit=270):
for _ in range(int(seconds / 0.2)):
rr = safe_read(robot, "机器人", robot.read_holding_registers,
address=addr_quit, count=1)
if rr and rr.registers[0] == 1:
print("检测到 R270=1立即退出循环")
return True
time.sleep(0.2)
return False
# =================== 初始化 ===================
plc = ModbusTcpClient(PLC_IP, port=PLC_PORT)
robot = ModbusTcpClient(ROBOT_IP, port=ROBOT_PORT)
if not plc.connect():
print("无法连接 PLC")
exit()
if not robot.connect():
print("无法连接 机器人")
plc.close()
exit()
print("✅ PLC 与 机器人连接成功")
time.sleep(0.5)
# 伺服使能
if safe_write(plc, "PLC", plc.write_coil, address=10, value=True):
print("✅ 伺服使能成功 (M10=True)")
else:
print("⚠️ 伺服使能失败")
# 初始化扫码 CSV
with open(SCAN_CSV_FILE, "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerow(["Bottle_No", "Scan_Result", "Time"])
bottle_count = 0
print("🟢 等待机器人触发信号... (R260=1扫码 / R256=1拉曼 / R270=1退出)")
# =================== 主监听循环 ===================
while True:
plc = ensure_connected(plc, "PLC", PLC_IP, PLC_PORT) or plc
robot = ensure_connected(robot, "机器人", ROBOT_IP, ROBOT_PORT) or robot
# 退出命令检测
quit_signal = safe_read(robot, "机器人", robot.read_holding_registers,
address=270, count=1)
if quit_signal and quit_signal.registers[0] == 1:
print("🟥 检测到 R270=1准备退出程序...")
break
# 读取关键寄存器
rr = safe_read(robot, "机器人", robot.read_holding_registers,
address=256, count=5)
if not rr:
time.sleep(0.3)
continue
r256, _, r258, r259, r260 = rr.registers[:5]
# ----------- 扫码部分 (R260=1) -----------
if r260 == 1:
bottle_count += 1
print(f"📸 第 {bottle_count} 瓶触发扫码 (R260=1)")
try:
result = scan_once(ip="192.168.1.50", port_in=2001, port_out=2002)
if result:
print(f"✅ 扫码成功: {result}")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(SCAN_CSV_FILE, "a", newline="", encoding="utf-8") as f:
csv.writer(f).writerow([bottle_count, result, timestamp])
else:
print("⚠️ 扫码失败或无返回")
except Exception as e:
print(f"❌ 扫码异常: {e}")
safe_write(robot, "机器人", robot.write_register, address=260, value=0)
time.sleep(0.2)
safe_write(robot, "机器人", robot.write_register, address=261, value=1)
print("➡️ 扫码完成 (R260→0, R261→1)")
# ----------- 拉曼 + 电机部分 (R256=1) -----------
if r256 == 1:
print("⚙️ 检测到 R256=1放瓶完成")
# 电机右转
safe_write(plc, "PLC", plc.write_register, address=1199, value=1)
safe_write(plc, "PLC", plc.write_register, address=1200, value=1)
print("➡️ 电机右转中...")
if wait_with_quit_check(robot, 3):
break
safe_write(plc, "PLC", plc.write_register, address=1199, value=0)
print("✅ 电机右转完成")
# 拉曼测试
print("🧪 开始拉曼测试...")
try:
success, file_prefix, df = run_raman_test(
integration_time=5000,
laser_power=200,
save_csv=True,
save_plot=True,
normalize=True,
norm_max=1.0
)
if success:
print(f"✅ 拉曼完成:{file_prefix}.csv / .png")
else:
print("⚠️ 拉曼失败")
except Exception as e:
print(f"❌ 拉曼测试异常: {e}")
# 电机左转
safe_write(plc, "PLC", plc.write_register, address=1299, value=1)
safe_write(plc, "PLC", plc.write_register, address=1300, value=1)
print("⬅️ 电机左转中...")
if wait_with_quit_check(robot, 3):
break
safe_write(plc, "PLC", plc.write_register, address=1299, value=0)
print("✅ 电机左转完成")
# 写入拉曼完成信号
safe_write(robot, "机器人", robot.write_register, address=257, value=1)
print("✅ 已写入 R257=1拉曼完成")
# 延迟清零 R256
print("⏳ 延迟4秒后清零 R256")
if wait_with_quit_check(robot, 4):
break
safe_write(robot, "机器人", robot.write_register, address=256, value=0)
print("✅ 已清零 R256")
# 等待机器人清零 R257
print("等待 R257 清零中...")
while True:
rr2 = safe_read(robot, "机器人", robot.read_holding_registers, address=257, count=1)
if rr2 and rr2.registers[0] == 0:
print("✅ 检测到 R257=0准备下一循环")
break
if wait_with_quit_check(robot, 1):
break
time.sleep(0.2)
time.sleep(0.2)
# =================== 程序退出清理 ===================
print("🧹 开始清理...")
safe_write(plc, "PLC", plc.write_coil, address=10, value=False)
for addr in [256, 257, 260, 261, 270]:
safe_write(robot, "机器人", robot.write_register, address=addr, value=0)
plc.close()
robot.close()
print("✅ 程序已退出,设备全部复位。")

View File

@@ -1,8 +1,23 @@
{
"nodes": [
{
"id": "bioyond_cell_workstation",
"name": "配液分液工站",
"children": [
],
"parent": null,
"type": "device",
"class": "bioyond_cell",
"config": {
"protocol_type": [],
"station_resource": {}
},
"data": {}
},
{
"id": "BatteryStation",
"name": "扣电工作站",
"name": "扣电组装工作站",
"children": [
"coin_cell_deck"
],
@@ -98,7 +113,7 @@
"z": 0
},
"config": {
"type": "ClipMagazine_four",
"type": "MagazineHolder_4",
"size_x": 80,
"size_y": 80,
"size_z": 10,
@@ -139,7 +154,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -234,7 +249,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -329,7 +344,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -424,7 +439,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -522,7 +537,7 @@
"z": 0
},
"config": {
"type": "ClipMagazine_four",
"type": "MagazineHolder_4",
"size_x": 80,
"size_y": 80,
"size_z": 10,
@@ -563,7 +578,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -658,7 +673,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -753,7 +768,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -848,7 +863,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -948,7 +963,7 @@
"z": 0
},
"config": {
"type": "ClipMagazine",
"type": "MagazineHolder_6",
"size_x": 80,
"size_y": 80,
"size_z": 10,
@@ -991,7 +1006,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1086,7 +1101,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1181,7 +1196,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1276,7 +1291,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1371,7 +1386,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1466,7 +1481,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1566,7 +1581,7 @@
"z": 0
},
"config": {
"type": "ClipMagazine",
"type": "MagazineHolder_6",
"size_x": 80,
"size_y": 80,
"size_z": 10,
@@ -1609,7 +1624,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1704,7 +1719,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1799,7 +1814,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1894,7 +1909,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -1989,7 +2004,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2084,7 +2099,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2184,7 +2199,7 @@
"z": 0
},
"config": {
"type": "ClipMagazine",
"type": "MagazineHolder_6",
"size_x": 80,
"size_y": 80,
"size_z": 10,
@@ -2227,7 +2242,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2322,7 +2337,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2417,7 +2432,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2512,7 +2527,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2607,7 +2622,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2702,7 +2717,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2802,7 +2817,7 @@
"z": 0
},
"config": {
"type": "ClipMagazine",
"type": "MagazineHolder_6",
"size_x": 80,
"size_y": 80,
"size_z": 10,
@@ -2845,7 +2860,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -2940,7 +2955,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3035,7 +3050,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3130,7 +3145,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3225,7 +3240,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3320,7 +3335,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3420,7 +3435,7 @@
"z": 0
},
"config": {
"type": "ClipMagazine",
"type": "MagazineHolder_6",
"size_x": 80,
"size_y": 80,
"size_z": 10,
@@ -3463,7 +3478,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3558,7 +3573,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3653,7 +3668,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3748,7 +3763,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3843,7 +3858,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -3938,7 +3953,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -4038,7 +4053,7 @@
"z": 0
},
"config": {
"type": "ClipMagazine",
"type": "MagazineHolder_6",
"size_x": 80,
"size_y": 80,
"size_z": 10,
@@ -4081,7 +4096,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -4176,7 +4191,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -4271,7 +4286,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -4366,7 +4381,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -4461,7 +4476,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,
@@ -4556,7 +4571,7 @@
"z": 10
},
"config": {
"type": "ClipMagazineHole",
"type": "Magazine",
"size_x": 14.0,
"size_y": 14.0,
"size_z": 10.0,

View File

@@ -0,0 +1,7 @@
material_name
LiPF6
LiDFOB
DTD
LiFSI
LiPO2F2
1 material_name
2 LiPF6
3 LiDFOB
4 DTD
5 LiFSI
6 LiPO2F2

View File

@@ -47,8 +47,8 @@ class BioyondV1RPC(BaseRequest):
super().__init__()
print("开始初始化 BioyondV1RPC")
self.config = config
self.api_key = config["api_key"]
self.host = config["api_host"]
self.api_key = config.get("api_key", "")
self.host = config.get("api_host", "") or config.get("base_url", "")
self._logger = SimpleLogger()
self.material_cache = {}
self._load_material_cache()
@@ -61,7 +61,7 @@ class BioyondV1RPC(BaseRequest):
:return: 当前时间的 ISO 8601 格式字符串
"""
current_time = datetime.now(timezone.utc).isoformat(
current_time = datetime.now().isoformat(
timespec='milliseconds'
)
# 替换时区部分为 'Z'
@@ -233,7 +233,7 @@ class BioyondV1RPC(BaseRequest):
return response.get("data", {})
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
"""指定库位出库物料(通过库位名称)"""
"""指定库位出库物料"""
location_id = LOCATION_MAPPING.get(location_name, location_name)
params = {
@@ -251,36 +251,7 @@ class BioyondV1RPC(BaseRequest):
})
if not response or response['code'] != 1:
return None
return response
def material_outbound_by_id(self, material_id: str, location_id: str, quantity: int) -> dict:
"""指定库位出库物料直接使用location_id
Args:
material_id: 物料ID
location_id: 库位ID不是库位名称是UUID
quantity: 数量
Returns:
dict: API响应失败返回None
"""
params = {
"materialId": material_id,
"locationId": location_id,
"quantity": quantity
}
response = self.post(
url=f'{self.host}/api/lims/storage/outbound',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": params
})
if not response or response['code'] != 1:
return None
return {}
return response
# ==================== 工作流查询相关接口 ====================
@@ -732,10 +703,10 @@ class BioyondV1RPC(BaseRequest):
"""预加载材料列表到缓存中"""
try:
print("正在加载材料列表缓存...")
# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
material_types = [1, 2]
for type_mode in material_types:
print(f"正在加载类型 {type_mode} 的材料...")
stock_query = f'{{"typeMode": {type_mode}, "includeDetail": true}}'
@@ -752,7 +723,7 @@ class BioyondV1RPC(BaseRequest):
material_id = material.get("id")
if material_name and material_id:
self.material_cache[material_name] = material_id
# 处理样品板等容器中的detail材料
detail_materials = material.get("detail", [])
for detail_material in detail_materials:

View File

@@ -2,137 +2,330 @@
"""
配置文件 - 包含所有配置信息和映射关系
"""
import os
# API配置
# ==================== API 基础配置 ====================
# BioyondCellWorkstation 默认配置(包含所有必需参数)
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': '反应器取出'
# API 连接配置
# "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.1.143:44389"),#实机
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.11.219:44388"),# 仿真机
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")),
# 报送配置
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
# HTTP 服务配置
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.16.11.2"), # HTTP服务监听地址监听计算机飞连ip地址
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
"debug_mode": False,# 调试模式
}
# 库位映射配置
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"
"A01": "3a19da56-1379-ff7c-1745-07e200b44ce2",
"B01": "3a19da56-1379-2424-d751-fe6e94cef938",
"C01": "3a19da56-1379-271c-03e3-6bdb590e395e",
"D01": "3a19da56-1379-277f-2b1b-0d11f7cf92c6",
"E01": "3a19da56-1379-2f1c-a15b-e01db90eb39a",
"F01": "3a19da56-1379-3fa1-846b-088158ac0b3d",
"G01": "3a19da56-1379-5aeb-d0cd-d3b4609d66e1",
"H01": "3a19da56-1379-6077-8258-bdc036870b78",
"I01": "3a19da56-1379-863b-a120-f606baf04617",
"J01": "3a19da56-1379-8a74-74e5-35a9b41d4fd5",
"K01": "3a19da56-1379-b270-b7af-f18773918abe",
"L01": "3a19da56-1379-ba54-6d78-fd770a671ffc",
"M01": "3a19da56-1379-c22d-c96f-0ceb5eb54a04",
"N01": "3a19da56-1379-d64e-c6c5-c72ea4829888",
"O01": "3a19da56-1379-d887-1a3c-6f9cce90f90e",
"P01": "3a19da56-1379-e77d-0e65-7463b238a3b9",
"Q01": "3a19da56-1379-edf6-1472-802ddb628774",
"R01": "3a19da56-1379-f281-0273-e0ef78f0fd97",
"S01": "3a19da56-1379-f924-7f68-df1fa51489f4",
"T01": "3a19da56-1379-ff7c-1745-07e200b44ce2"
}
},
"溶液堆栈": {
"配液站内试剂仓库": {
"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"
"A01": "3a19da43-57b5-294f-d663-154a1cc32270",
"B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2",
"C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f",
"A02": "3a19da43-57b5-8441-db94-b4d3875a4b6c",
"B02": "3a19da43-57b5-3e41-c181-5119dddaf50c",
"C02": "3a19da43-57b5-269b-282d-fba61fe8ce96",
"A03": "3a19da43-57b5-7c1e-d02e-c40e8c33f8a1",
"B03": "3a19da43-57b5-659f-621f-1dcf3f640363",
"C03": "3a19da43-57b5-855a-6e71-f398e376dee1",
}
},
"试剂堆栈": {
"试剂替换仓库": {
"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"
"A01": "3a19da51-8f4e-30f3-ea08-4f8498e9b097",
"B01": "3a19da51-8f4e-1da7-beb0-80a4a01e67a8",
"C01": "3a19da51-8f4e-337d-2675-bfac46880b06",
"D01": "3a19da51-8f4e-e514-b92c-9c44dc5e489d",
"E01": "3a19da51-8f4e-22d1-dd5b-9774ddc80402",
"F01": "3a19da51-8f4e-273a-4871-dff41c29bfd9",
"G01": "3a19da51-8f4e-b32f-454f-74bc1a665653",
"H01": "3a19da51-8f4e-8c93-68c9-0b4382320f59",
"I01": "3a19da51-8f4e-360c-0149-291b47c6089b",
"J01": "3a19da51-8f4e-4152-9bca-8d64df8c1af0"
}
},
"自动堆栈-左": {
"uuid": "",
"site_uuids": {
"A01": "3a19debc-84b5-4c1c-d3a1-26830cf273ff",
"A02": "3a19debc-84b5-033b-b31f-6b87f7c2bf52",
"B01": "3a19debc-84b5-3924-172f-719ab01b125c",
"B02": "3a19debc-84b5-aad8-70c6-b8c6bb2d8750"
}
},
"自动堆栈-右": {
"uuid": "",
"site_uuids": {
"A01": "3a19debe-5200-7df2-1dd9-7d202f158864",
"A02": "3a19debe-5200-573b-6120-8b51f50e1e50",
"B01": "3a19debe-5200-7cd8-7666-851b0a97e309",
"B02": "3a19debe-5200-e6d3-96a3-baa6e3d5e484"
}
},
"手动堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe",
"A03": "3a19deae-2c7a-5876-c454-6b7e224ca927",
"B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1",
"B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3",
"B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a",
"C01": "3a19deae-2c7a-32bc-768e-556647e292f3",
"C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4",
"C03": "3a19deae-2c7a-3056-6504-10dc73fbc276",
"D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440",
"D02": "3a19deae-2c7a-61be-601c-b6fb5610499a",
"D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560",
"E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363",
"E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910",
"E03": "3a19deae-2c7a-b163-2219-23df15200311",
"F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a",
"F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679",
"F03": "3a19deae-2c7a-f7c4-12bd-425799425698",
"G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955",
"G02": "3a19deae-2c7a-204e-95ed-1f1950f28343",
"G03": "3a19deae-2c7a-392b-62f1-4907c66343f8",
"H01": "3a19deae-2c7a-5602-e876-d27aca4e3201",
"H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702",
"H03": "3a19deae-2c7a-780b-8965-2e1345f7e834",
"I01": "3a19deae-2c7a-8849-e172-07de14ede928",
"I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0",
"I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4",
"J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6",
"J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205",
"J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9"
}
},
"4号手套箱内部堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa20-a7b1-c665-8b9c-d8099d07d2f6",
"A02": "3a1baa20-a7b1-93a7-c988-f9c8ad6c58c9",
"A03": "3a1baa20-a7b1-00ee-f751-da9b20b6c464",
"A04": "3a1baa20-a7b1-4712-c37b-0b5b658ef7b9",
"B01": "3a1baa20-a7b1-9847-fc9c-96d604cd1a8e",
"B02": "3a1baa20-a7b1-4ae9-e604-0601db06249c",
"B03": "3a1baa20-a7b1-8329-ea75-81ca559d9ce1",
"B04": "3a1baa20-a7b1-89c5-d96f-36e98a8f7268",
"C01": "3a1baa20-a7b1-32ec-39e6-8044733839d6",
"C02": "3a1baa20-a7b1-b573-e426-4c86040348b2",
"C03": "3a1baa20-a7b1-cca7-781e-0522b729bf5d",
"C04": "3a1baa20-a7b1-7c98-5fd9-5855355ae4b3"
}
},
"大分液瓶堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19da3d-4f3d-bcac-2932-7542041e10e0",
"A02": "3a19da3d-4f3d-4d75-38ac-fb58ad0687c3",
"A03": "3a19da3d-4f3d-b25e-f2b1-85342a5b7eae",
"B01": "3a19da3d-4f3d-fd3e-058a-2733a0925767",
"B02": "3a19da3d-4f3d-37bd-a944-c391ad56857f",
"B03": "3a19da3d-4f3d-e353-7862-c6d1d4bc667f",
"C01": "3a19da3d-4f3d-9519-5da7-76179c958e70",
"C02": "3a19da3d-4f3d-b586-d7ed-9ec244f6f937",
"C03": "3a19da3d-4f3d-5061-249b-35dfef732811"
}
},
"小分液瓶堆栈": {
"uuid": "",
"site_uuids": {
"C03": "3a19da40-55bf-8943-d20d-a8b3ea0d16c0"
}
},
"站内Tip头盒堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19deab-d5cc-be1e-5c37-4e9e5a664388",
"A02": "3a19deab-d5cc-b394-8141-27cb3853e8ea",
"B01": "3a19deab-d5cc-4dca-596e-ca7414d5f501",
"B02": "3a19deab-d5cc-9bc0-442b-12d9d59aa62a",
"C01": "3a19deab-d5cc-2eaf-b6a4-f0d54e4f1246",
"C02": "3a19deab-d5cc-d9f4-25df-b8018c372bc7"
}
},
"配液站内配液大板仓库(无需提前上料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a21dc-06af-3915-9cb9-80a9dc42f386"
}
},
"配液站内配液小板仓库(无需以前入料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a21de-8e8b-7938-2d06-858b36c10e31"
}
},
"移液站内大瓶板仓库(无需提前如料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a224c-c727-fa62-1f2b-0037a84b9fca"
}
},
"移液站内小瓶板仓库(无需提前入料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a224d-ed49-710c-a9c3-3fc61d479cbb"
}
},
"适配器位仓库": {
"uuid": "",
"site_uuids": {
"A01": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c"
}
},
"1号2号手套箱交接堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa49-7f77-35aa-60b1-e55a45d065fa"
}
},
"2号手套箱内部堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa4b-393e-9f86-3921-7a18b0a8e371",
"A02": "3a1baa4b-393e-9425-928b-ee0f6f679d44",
"A03": "3a1baa4b-393e-0baf-632b-59dfdc931a3a",
"B01": "3a1baa4b-393e-f8aa-c8a9-956f3132f05c",
"B02": "3a1baa4b-393e-ef05-42f6-53f4c6e89d70",
"B03": "3a1baa4b-393e-c07b-a924-a9f0dfda9711",
"C01": "3a1baa4b-393e-4c2b-821a-16a7fe025c48",
"C02": "3a1baa4b-393e-2eaf-61a1-9063c832d5a2",
"C03": "3a1baa4b-393e-034e-8e28-8626d934a85f"
}
}
}
# 物料类型配置
MATERIAL_TYPE_MAPPINGS = {
"烧杯": ("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"),
"100ml液体": ("YB_100ml_yeti", "d37166b3-ecaa-481e-bd84-3032b795ba07"),
"": ("YB_ye", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
"高粘液": ("YB_gaonianye", "abe8df30-563d-43d2-85e0-cabec59ddc16"),
"加样头(大)": ("YB_jia_yang_tou_da_Carrier", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "加样头(大)板": ("YB_jia_yang_tou_da", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"),
"5ml分液瓶板": ("YB_5ml_fenyepingban", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"),
"5ml分液": ("YB_5ml_fenyeping", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"),
"20ml分液瓶板": ("YB_20ml_fenyepingban", "3a192fa4-47db-3449-162a-eaf8aba57e27"),
"20ml分液瓶": ("YB_20ml_fenyeping", "3a192c2b-19e8-f0a3-035e-041ca8ca1035"),
"配液瓶(小)板": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
"配液瓶(小)": ("YB_pei_ye_xiao_Bottle", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
"配液瓶(大)板": ("YB_peiyepingdaban", "53e50377-32dc-4781-b3c0-5ce45bc7dc27"),
"配液瓶(大)": ("YB_pei_ye_da_Bottle", "19c52ad1-51c5-494f-8854-576f4ca9c6ca"),
"适配器块": ("YB_shi_pei_qi_kuai", "efc3bb32-d504-4890-91c0-b64ed3ac80cf"),
"枪头盒": ("YB_qiang_tou_he", "3a192c2e-20f3-a44a-0334-c8301839d0b3"),
"枪头": ("YB_qiang_tou", "b6196971-1050-46da-9927-333e8dea062d"),
}
# 步骤参数配置各工作流的步骤UUID
WORKFLOW_STEP_IDS = {
"reactor_taken_in": {
"config": ""
SOLID_LIQUID_MAPPINGS = {
# 固体
"LiDFOB": {
"typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
"code": "",
"barCode": "",
"name": "LiDFOB",
"unit": "g",
"parameters": "",
"quantity": "2",
"warningQuantity": "1",
"details": []
},
"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": ""
}
# "LiPF6": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiPF6",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# "LiFSI": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiFSI",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# "DTC": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "DTC",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# "LiPO2F2": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiPO2F2",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# 液体
# "SA": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "VC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "AND": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "HTCN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DENE": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "TMSP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "TMSB": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "SN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "FEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
}
WORKFLOW_MAPPINGS = {}
LOCATION_MAPPING = {}

View File

@@ -232,7 +232,7 @@ class BioyondReactionStation(BioyondWorkstation):
temperature: 温度设定(°C)
"""
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
if not volume and solvents is not None:
if volume is None and solvents is not None:
# 参数类型转换:如果是字符串则解析为字典
if isinstance(solvents, str):
try:

View File

@@ -74,6 +74,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
deck=self.workstation.deck
)
print("unilab_resources:",unilab_resources)
logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源")
return True
@@ -85,90 +86,8 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
def sync_to_external(self, resource: Any) -> bool:
"""将本地物料数据变更同步到Bioyond系统"""
try:
# ✅ 跳过仓库类型的资源 - 仓库是容器,不是物料
resource_category = getattr(resource, "category", None)
if resource_category == "warehouse":
logger.debug(f"[同步→Bioyond] 跳过仓库类型资源: {resource.name} (仓库是容器,不需要同步为物料)")
return True
logger.info(f"[同步→Bioyond] 收到物料变更: {resource.name}")
# 获取物料的 Bioyond ID
extra_info = getattr(resource, "unilabos_extra", {})
material_bioyond_id = extra_info.get("material_bioyond_id")
# ⭐ 如果没有 Bioyond ID尝试从 Bioyond 系统中按名称查询
if not material_bioyond_id:
logger.warning(f"[同步→Bioyond] 物料 {resource.name} 没有 Bioyond ID尝试按名称查询...")
try:
# 查询所有类型的物料0=耗材, 1=样品, 2=试剂
import json
all_materials = []
for type_mode in [0, 1, 2]:
query_params = json.dumps({
"typeMode": type_mode,
"filter": "", # 空字符串表示查询所有
"includeDetail": True
})
materials = self.bioyond_api_client.stock_material(query_params)
if materials:
all_materials.extend(materials)
logger.info(f"[同步→Bioyond] 查询到 {len(all_materials)} 个物料")
# 按名称匹配
for mat in all_materials:
if mat.get("name") == resource.name:
material_bioyond_id = mat.get("id")
mat_type = mat.get("typeName", "未知")
logger.info(f"✅ 找到物料 {resource.name} ({mat_type}) 的 Bioyond ID: {material_bioyond_id[:8]}...")
# 保存 ID 到资源对象
extra_info["material_bioyond_id"] = material_bioyond_id
setattr(resource, "unilabos_extra", extra_info)
break
if not material_bioyond_id:
logger.warning(f"⚠️ 在 Bioyond 系统中未找到名为 {resource.name} 的物料")
logger.info(f"[同步→Bioyond] 这是一个新物料,将创建并入库到 Bioyond 系统")
# 不返回,继续执行后续的创建+入库流程
except Exception as e:
logger.error(f"查询 Bioyond 物料失败: {e}")
import traceback
traceback.print_exc()
return False
# 检查是否有位置更新请求
update_site = extra_info.get("update_resource_site")
if not update_site:
logger.debug(f"[同步→Bioyond] 无位置更新请求")
return True
# ===== 物料移动/创建流程 =====
if material_bioyond_id:
logger.info(f"[同步→Bioyond] 🔄 开始移动物料 {resource.name}{update_site}")
else:
logger.info(f"[同步→Bioyond] 开始创建新物料 {resource.name} 并入库到 {update_site}") # 第1步获取仓库配置
from .config import WAREHOUSE_MAPPING
warehouse_mapping = WAREHOUSE_MAPPING
# 确定目标仓库名称(通过遍历所有仓库的库位配置)
parent_name = None
target_location_uuid = None
for warehouse_name, warehouse_info in warehouse_mapping.items():
site_uuids = warehouse_info.get("site_uuids", {})
if update_site in site_uuids:
parent_name = warehouse_name
target_location_uuid = site_uuids[update_site]
logger.info(f"[同步] 目标仓库: {parent_name}/{update_site}")
logger.info(f"[同步] 目标库位UUID: {target_location_uuid[:8]}...")
break
if not parent_name or not target_location_uuid:
logger.error(f"❌ 库位 {update_site} 没有在 WAREHOUSE_MAPPING 中配置")
logger.debug(f"可用仓库: {list(warehouse_mapping.keys())}")
if self.bioyond_api_client is None:
logger.error("Bioyond API客户端未初始化")
return False
bioyond_material = resource_plr_to_bioyond(
@@ -247,28 +166,30 @@ class BioyondWorkstation(WorkstationBase):
self.workflow_sequence = []
self.pending_task_params = []
if "workflow_mappings" in bioyond_config:
self._set_workflow_mappings(bioyond_config["workflow_mappings"])
if self.bioyond_config and "workflow_mappings" in self.bioyond_config:
self._set_workflow_mappings(self.bioyond_config["workflow_mappings"])
logger.info(f"Bioyond工作站初始化完成")
def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node
# ⭐ 上传 deck包括所有 warehouses 及其中的物料)
# 注意:如果有从 Bioyond 同步的物料,它们已经被放置到 warehouse 中了
# 所以只需要上传 deck物料会作为 warehouse 的 children 一起上传
logger.info("正在上传 deck包括 warehouses 和物料)到云端...")
print("~~~",self._ros_node)
print("deck",self.deck)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})
# 清理临时变量(物料已经在 deck 的 warehouse children 中,不需要单独上传)
if hasattr(self, "_synced_resources"):
logger.info(f"{len(self._synced_resources)} 个从Bioyond同步的物料已包含在 deck 中")
self._synced_resources = []
def resource_tree_transfer(self, old_parent: ResourcePLR, plr_resource: ResourcePLR, parent_resource: ResourcePLR):
# ROS2DeviceNode.run_async_func(self._ros_node.resource_tree_transfer, True, **{
# "old_parent": old_parent,
# "plr_resource": plr_resource,
# "parent_resource": parent_resource,
# })
print("resource_tree_transfer", plr_resource, parent_resource)
if hasattr(plr_resource, "unilabos_data") and plr_resource.unilabos_data:
if "update_resource_site" in plr_resource.unilabos_data:
site = plr_resource.unilabos_data["update_resource_site"]
return
self.lab_logger().warning(f"无库位的上料,不处理,{plr_resource} 挂载到 {parent_resource}")
def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
time.sleep(3)
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
"plr_resources": resource,
"target_device_id": mount_device_id,
@@ -339,7 +260,7 @@ class BioyondWorkstation(WorkstationBase):
}
# ==================== 工作流合并与参数设置 API ====================
def append_to_workflow_sequence(self, web_workflow_name: str) -> bool:
# 检查是否为JSON格式的字符串
actual_workflow_name = web_workflow_name
@@ -350,7 +271,7 @@ class BioyondWorkstation(WorkstationBase):
print(f"解析JSON格式工作流名称: {web_workflow_name} -> {actual_workflow_name}")
except json.JSONDecodeError:
print(f"JSON解析失败使用原始字符串: {web_workflow_name}")
workflow_id = self._get_workflow(actual_workflow_name)
if workflow_id:
self.workflow_sequence.append(workflow_id)
@@ -415,7 +336,7 @@ class BioyondWorkstation(WorkstationBase):
# ============ 工作站状态管理 ============
def get_station_info(self) -> Dict[str, Any]:
"""获取工作站基础信息
Returns:
Dict[str, Any]: 工作站基础信息包括设备ID、状态等
"""
@@ -543,8 +464,8 @@ class BioyondWorkstation(WorkstationBase):
# 转换为UniLab格式
unilab_resources = resource_bioyond_to_plr(
bioyond_data,
type_mapping=self.bioyond_config["material_type_mappings"],
bioyond_data,
type_mapping=self.bioyond_config["material_type_mappings"],
deck=self.deck
)
@@ -561,7 +482,7 @@ def create_bioyond_workstation_example():
"""创建Bioyond工作站示例"""
# 配置参数
device_id = "bioyond_workstation_001"
device_id = "bioyond_cell_workstation_001"
# 子资源配置
children = {
@@ -580,8 +501,8 @@ def create_bioyond_workstation_example():
# Bioyond配置
bioyond_config = {
"base_url": "http://bioyond.example.com/api",
"api_key": "your_api_key_here",
"base_url": "http://172.16.11.219:44388",
"api_key": "8A819E5C",
"sync_interval": 60, # 60秒同步一次
"timeout": 30
}
@@ -605,4 +526,18 @@ def create_bioyond_workstation_example():
if __name__ == "__main__":
by = create_bioyond_workstation_example()
by.get_workstation_status()
by.get_device_status()
by.get_bioyond_status()
by.get_station_info()
by.get_workstation_status()
by.get_bioyond_status()
by.get_station_info()
by.get_workstation_status()
by.get_bioyond_status()
by.get_station_info()
by.get_workstation_status()
by.get_bioyond_status()
by.get_station_info()
pass

View File

@@ -0,0 +1,639 @@
"""
纽扣电池组装工作站物料类定义
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))
if __name__ == "__main__":
deck = create_coin_cell_deck()
print(deck)

View File

@@ -1,44 +1,134 @@
import csv
import inspect
import json
import os
import threading
import time
import types
from datetime import datetime
from typing import Any, Dict, Optional
from pylabrobot.resources import Resource as PLRResource
from functools import wraps
from pylabrobot.resources import Deck, Resource as PLRResource
from unilabos_msgs.msg import Resource
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import MaterialHole, MaterialPlate
from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient
from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import *
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import *
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
from unilabos.resources.graphio import convert_resources_to_type
from unilabos.utils.log import logger
def _ensure_modbus_slave_kw_alias(modbus_client):
if modbus_client is None:
return
method_names = [
"read_coils",
"write_coils",
"write_coil",
"read_discrete_inputs",
"read_holding_registers",
"write_register",
"write_registers",
]
def _wrap(func):
signature = inspect.signature(func)
has_var_kwargs = any(param.kind == param.VAR_KEYWORD for param in signature.parameters.values())
accepts_unit = has_var_kwargs or "unit" in signature.parameters
accepts_slave = has_var_kwargs or "slave" in signature.parameters
@wraps(func)
def _wrapped(self, *args, **kwargs):
if "slave" in kwargs and not accepts_slave:
slave_value = kwargs.pop("slave")
if accepts_unit and "unit" not in kwargs:
kwargs["unit"] = slave_value
if "unit" in kwargs and not accepts_unit:
unit_value = kwargs.pop("unit")
if accepts_slave and "slave" not in kwargs:
kwargs["slave"] = unit_value
return func(self, *args, **kwargs)
_wrapped._has_slave_alias = True
return _wrapped
for name in method_names:
if not hasattr(modbus_client, name):
continue
bound_method = getattr(modbus_client, name)
func = getattr(bound_method, "__func__", None)
if func is None:
continue
if getattr(func, "_has_slave_alias", False):
continue
wrapped = _wrap(func)
setattr(modbus_client, name, types.MethodType(wrapped, modbus_client))
def _coerce_deck_input(deck: Any) -> Optional[Deck]:
if deck is None:
return None
if isinstance(deck, Deck):
return deck
if isinstance(deck, PLRResource):
return deck if isinstance(deck, Deck) else None
candidates = None
if isinstance(deck, dict):
if "nodes" in deck and isinstance(deck["nodes"], list):
candidates = deck["nodes"]
else:
candidates = [deck]
elif isinstance(deck, list):
candidates = deck
if candidates is None:
return None
try:
converted = convert_resources_to_type(resources_list=candidates, resource_type=Deck)
if isinstance(converted, Deck):
return converted
if isinstance(converted, list):
for item in converted:
if isinstance(item, Deck):
return item
except Exception as exc:
logger.warning(f"deck 转换 Deck 失败: {exc}")
return None
#构建物料系统
class CoinCellAssemblyWorkstation(WorkstationBase):
def __init__(
self,
deck: CoincellDeck,
address: str = "192.168.1.20",
def __init__(self,
config: dict = None,
deck=None,
address: str = "172.16.28.102",
port: str = "502",
debug_mode: bool = True,
debug_mode: bool = False,
*args,
**kwargs,
):
super().__init__(
#桌子
deck=deck,
*args,
**kwargs,
)
**kwargs):
if deck is None and config:
deck = config.get('deck')
if deck is None:
logger.info("没有传入依华deck检查启动json文件")
super().__init__(deck=deck, *args, **kwargs,)
self.debug_mode = debug_mode
self.deck = deck
""" 连接初始化 """
modbus_client = TCPClient(addr=address, port=port)
print("modbus_client", modbus_client)
logger.debug(f"创建 Modbus 客户端: {modbus_client}")
_ensure_modbus_slave_kw_alias(modbus_client.client)
if not debug_mode:
modbus_client.client.connect()
count = 100
@@ -49,27 +139,21 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
time.sleep(2)
if not modbus_client.client.is_socket_open():
raise ValueError('modbus tcp connection failed')
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_1105.csv'))
self.client = modbus_client.register_node_list(self.nodes)
else:
print("测试模式,跳过连接")
self.nodes, self.client = None, None
""" 工站的配置 """
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv'))
self.client = modbus_client.register_node_list(self.nodes)
self.success = False
self.allow_data_read = False #允许读取函数运行标志位
self.csv_export_thread = None
self.csv_export_running = False
self.csv_export_file = None
#创建一个物料台面,包含两个极片板
#self.deck = create_a_coin_cell_deck()
#self._ros_node.update_resource(self.deck)
#ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
# "resources": [self.deck]
#})
self.coin_num_N = 0 #已组装电池数量
def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node
#self.deck = create_a_coin_cell_deck()
@@ -491,11 +575,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
try:
# 尝试不同的字节序读取
code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE)
print(code_little)
# logger.debug(f"读取电池二维码原始数据: {code_little}")
clean_code = code_little[-8:][::-1]
return clean_code
except Exception as e:
print(f"读取电池二维码失败: {e}")
logger.error(f"读取电池二维码失败: {e}")
return "N/A"
@@ -504,11 +588,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
try:
# 尝试不同的字节序读取
code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE)
print(code_little)
# logger.debug(f"读取电解液二维码原始数据: {code_little}")
clean_code = code_little[-8:][::-1]
return clean_code
except Exception as e:
print(f"读取电解液二维码失败: {e}")
logger.error(f"读取电解液二维码失败: {e}")
return "N/A"
# ===================== 环境监控区 ======================
@@ -606,7 +690,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
print("waiting for start_cmd")
time.sleep(1)
def func_pack_send_bottle_num(self, bottle_num: int):
def func_pack_send_bottle_num(self, bottle_num):
bottle_num = int(bottle_num)
#发送电解液平台数
print("启动")
while (self._unilab_rece_electrolyte_bottle_num()) == False:
@@ -654,16 +739,25 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# self.success = True
# return self.success
def func_pack_send_msg_cmd(self, elec_use_num) -> bool:
def func_pack_send_msg_cmd(self, elec_use_num, elec_vol, assembly_type, assembly_pressure) -> bool:
"""UNILAB写参数"""
while (self.request_rec_msg_status) == False:
print("wait for request_rec_msg_status to True")
time.sleep(1)
self.success = False
#self._unilab_send_msg_electrolyte_num(elec_num)
time.sleep(1)
#设置平行样数目
self._unilab_send_msg_electrolyte_use_num(elec_use_num)
time.sleep(1)
#发送电解液加注量
self._unilab_send_msg_electrolyte_vol(elec_vol)
time.sleep(1)
#发送电解液组装类型
self._unilab_send_msg_assembly_type(assembly_type)
time.sleep(1)
#发送电池压制力
self._unilab_send_msg_assembly_pressure(assembly_pressure)
time.sleep(1)
self._unilab_send_msg_succ_cmd(True)
time.sleep(1)
while (self.request_rec_msg_status) == True:
@@ -688,15 +782,32 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
data_coin_num = self.data_coin_num
data_electrolyte_code = self.data_electrolyte_code
data_coin_cell_code = self.data_coin_cell_code
print("data_open_circuit_voltage", data_open_circuit_voltage)
print("data_pole_weight", data_pole_weight)
print("data_assembly_time", data_assembly_time)
print("data_assembly_pressure", data_assembly_pressure)
print("data_electrolyte_volume", data_electrolyte_volume)
print("data_coin_num", data_coin_num)
print("data_electrolyte_code", data_electrolyte_code)
print("data_coin_cell_code", data_coin_cell_code)
logger.debug(f"data_open_circuit_voltage: {data_open_circuit_voltage}")
logger.debug(f"data_pole_weight: {data_pole_weight}")
logger.debug(f"data_assembly_time: {data_assembly_time}")
logger.debug(f"data_assembly_pressure: {data_assembly_pressure}")
logger.debug(f"data_electrolyte_volume: {data_electrolyte_volume}")
logger.debug(f"data_coin_num: {data_coin_num}")
logger.debug(f"data_electrolyte_code: {data_electrolyte_code}")
logger.debug(f"data_coin_cell_code: {data_coin_cell_code}")
#接收完信息后读取完毕标志位置True
liaopan3 = self.deck.get_resource("成品弹夹")
#把物料解绑后放到另一盘上
battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2)
battery._unilabos_state = {
"electrolyte_name": data_coin_cell_code,
"data_electrolyte_code": data_electrolyte_code,
"open_circuit_voltage": data_open_circuit_voltage,
"assembly_pressure": data_assembly_pressure,
"electrolyte_volume": data_electrolyte_volume
}
liaopan3.children[self.coin_num_N].assign_child_resource(battery, location=None)
#print(jipian2.parent)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})
self._unilab_rec_msg_succ_cmd(True)
time.sleep(1)
#等待允许读取标志位置False
@@ -754,11 +865,25 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self.success = True
return self.success
def qiming_coin_cell_code(self, fujipian_panshu:int, fujipian_juzhendianwei:int=0, gemopanshu:int=0, gemo_juzhendianwei:int=0, lvbodian:bool=True, battery_pressure_mode:bool=True, battery_pressure:int=4000, battery_clean_ignore:bool=False) -> bool:
self.success = False
self.client.use_node('REG_MSG_NE_PLATE_NUM').write(fujipian_panshu)
self.client.use_node('REG_MSG_NE_PLATE_MATRIX').write(fujipian_juzhendianwei)
self.client.use_node('REG_MSG_SEPARATOR_PLATE_NUM').write(gemopanshu)
self.client.use_node('REG_MSG_SEPARATOR_PLATE_MATRIX').write(gemo_juzhendianwei)
self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian)
self.client.use_node('REG_MSG_PRESS_MODE').write(not battery_pressure_mode)
# self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(battery_pressure)
self.client.use_node('REG_MSG_BATTERY_CLEAN_IGNORE').write(battery_clean_ignore)
self.success = True
return self.success
def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell_data") -> bool:
def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="/Users/sml/work") -> bool:
elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure)
summary_csv_file = os.path.join(file_path, "duandian.csv")
# 如果断点文件存在,先读取之前的进度
if os.path.exists(summary_csv_file):
read_status_flag = True
with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile:
@@ -784,54 +909,38 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
elec_num_N = 0
elec_use_num_N = 0
coin_num_N = 0
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
for i in range(20):
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}")
print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}")
#如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。
if read_status_flag == False:
pass
#初始化
self.func_pack_device_init()
#self.func_pack_device_init()
#切换自动
self.func_pack_device_auto()
#self.func_pack_device_auto()
#启动,小车收回
self.func_pack_device_start()
#self.func_pack_device_start()
#发送电解液瓶数量,启动搬运,多搬运没事
self.func_pack_send_bottle_num(elec_num)
#self.func_pack_send_bottle_num(elec_num)
last_i = elec_num_N
last_j = elec_use_num_N
for i in range(last_i, elec_num):
print(f"开始第{last_i+i+1}瓶电解液的组装")
#第一个循环从上次断点继续后续循环从0开始
j_start = last_j if i == last_i else 0
self.func_pack_send_msg_cmd(elec_use_num-j_start)
self.func_pack_send_msg_cmd(elec_use_num-j_start, elec_vol, assembly_type, assembly_pressure)
for j in range(j_start, elec_use_num):
print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装")
#读取电池组装数据并存入csv
self.func_pack_get_msg_cmd(file_path)
time.sleep(1)
#这里定义物料系统
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
liaopan1 = self.deck.get_resource("liaopan1")
liaopan4 = self.deck.get_resource("liaopan4")
jipian1 = liaopan1.children[coin_num_N].children[0]
jipian4 = liaopan4.children[coin_num_N].children[0]
#print(jipian1)
#从料盘上去物料解绑后放到另一盘上
jipian1.parent.unassign_child_resource(jipian1)
jipian4.parent.unassign_child_resource(jipian4)
#print(jipian2.parent)
battery = Battery(name = f"battery_{coin_num_N}")
battery.assign_child_resource(jipian1, location=None)
battery.assign_child_resource(jipian4, location=None)
zidanjia6 = self.deck.get_resource("zi_dan_jia6")
zidanjia6.children[0].assign_child_resource(battery, location=None)
# 生成断点文件
# 生成包含elec_num_N、coin_num_N、timestamp的CSV文件
@@ -842,6 +951,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp])
csvfile.flush()
coin_num_N += 1
self.coin_num_N = coin_num_N
elec_use_num_N += 1
elec_num_N += 1
elec_use_num_N = 0
@@ -878,36 +988,27 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
def fun_wuliao_test(self) -> bool:
#找到data_init中构建的2个物料盘
#liaopan1 = self.deck.get_resource("liaopan1")
#liaopan4 = self.deck.get_resource("liaopan4")
#for coin_num_N in range(16):
# liaopan1 = self.deck.get_resource("liaopan1")
# liaopan4 = self.deck.get_resource("liaopan4")
# jipian1 = liaopan1.children[coin_num_N].children[0]
# jipian4 = liaopan4.children[coin_num_N].children[0]
# #print(jipian1)
# #从料盘上去物料解绑后放到另一盘上
# jipian1.parent.unassign_child_resource(jipian1)
# jipian4.parent.unassign_child_resource(jipian4)
#
# #print(jipian2.parent)
# battery = Battery(name = f"battery_{coin_num_N}")
# battery.assign_child_resource(jipian1, location=None)
# battery.assign_child_resource(jipian4, location=None)
#
# zidanjia6 = self.deck.get_resource("zi_dan_jia6")
# zidanjia6.children[0].assign_child_resource(battery, location=None)
# ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
# "resources": [self.deck]
# })
# time.sleep(2)
for i in range(20):
print(f"输出{i}")
time.sleep(2)
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
for i in range(16):
battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2)
battery._unilabos_state = {
"diameter": 20.0,
"height": 20.0,
"assembly_pressure": i,
"electrolyte_volume": 20.0,
"electrolyte_name": f"DP{i}"
}
liaopan3.children[i].assign_child_resource(battery, location=None)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})
# for i in range(40):
# print(f"fun_wuliao_test 运行结束{i}")
# time.sleep(1)
# time.sleep(40)
# 数据读取与输出
def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"):
def func_read_data_and_output(self, file_path: str="/Users/sml/work"):
# 检查CSV导出是否正在运行已运行则跳出防止同时启动两个while循环
if self.csv_export_running:
return False, "读取已在运行中"
@@ -1012,7 +1113,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# else:
# print("子弹夹洞位0没有极片")
#
# #把电解液从瓶中取到电池夹子中
# # TODO:#把电解液从瓶中取到电池夹子中
# battery_site = deck.get_resource("battery_press_1")
# clip_magazine_battery = deck.get_resource("clip_magazine_battery")
# if battery_site.has_battery():
@@ -1102,41 +1203,16 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
'''
if __name__ == "__main__":
from pylabrobot.resources import Resource
Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True)
#Coin_Cell.func_pack_device_init()
#Coin_Cell.func_pack_device_auto()
#Coin_Cell.func_pack_device_start()
#Coin_Cell.func_pack_send_bottle_num(2)
#Coin_Cell.func_pack_send_msg_cmd(2)
#Coin_Cell.func_pack_get_msg_cmd()
#Coin_Cell.func_pack_get_msg_cmd()
#Coin_Cell.func_pack_send_finished_cmd()
#
#Coin_Cell.func_allpack_cmd(3, 2)
#print(Coin_Cell.data_stack_vision_code)
#print("success")
#创建一个物料台面
#deck = create_a_coin_cell_deck()
##在台面上找到料盘和极片
#liaopan1 = deck.get_resource("liaopan1")
#liaopan2 = deck.get_resource("liaopan2")
#jipian1 = liaopan1.children[1].children[0]
#
##print(jipian1)
##把物料解绑后放到另一盘上
#jipian1.parent.unassign_child_resource(jipian1)
#liaopan2.children[1].assign_child_resource(jipian1, location=None)
##print(jipian2.parent)
from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type
with open("./button_battery_decks_unilab.json", "r", encoding="utf-8") as f:
bioyond_resources_unilab = json.load(f)
print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源")
ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource])
print(f"转换结果类型: {type(ulab_resources)}")
print(ulab_resources)
# 简单测试
workstation = CoinCellAssemblyWorkstation(deck=CoincellDeck(setup=True, name="coin_cell_deck"))
# workstation.qiming_coin_cell_code(fujipian_panshu=1, fujipian_juzhendianwei=2, gemopanshu=3, gemo_juzhendianwei=4, lvbodian=False, battery_pressure_mode=False, battery_pressure=4200, battery_clean_ignore=False)
# print(f"工作站创建成功: {workstation.deck.name}")
# print(f"料盘数量: {len(workstation.deck.children)}")
workstation.func_pack_device_init()
workstation.func_pack_device_auto()
workstation.func_pack_device_start()
workstation.func_pack_send_bottle_num(16)
workstation.func_allpack_cmd(elec_num=16, elec_use_num=16, elec_vol=50, assembly_type=7, assembly_pressure=4200, file_path="/Users/calvincao/Desktop/work/Uni-Lab-OS-hhm")

View File

@@ -0,0 +1,64 @@
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
COIL_SYS_START_CMD,BOOL,,,,coil,9010,
COIL_SYS_STOP_CMD,BOOL,,,,coil,9020,
COIL_SYS_RESET_CMD,BOOL,,,,coil,9030,
COIL_SYS_HAND_CMD,BOOL,,,,coil,9040,
COIL_SYS_AUTO_CMD,BOOL,,,,coil,9050,
COIL_SYS_INIT_CMD,BOOL,,,,coil,9060,
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,9700,
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,9710,unilab_rec_msg_succ_cmd
COIL_SYS_START_STATUS,BOOL,,,,coil,9210,
COIL_SYS_STOP_STATUS,BOOL,,,,coil,9220,
COIL_SYS_RESET_STATUS,BOOL,,,,coil,9230,
COIL_SYS_HAND_STATUS,BOOL,,,,coil,9240,
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,9250,
COIL_SYS_INIT_STATUS,BOOL,,,,coil,9260,
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,9500,
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,9510,request_send_msg_status
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,17000,
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,17002,unilab_send_msg_electrolyte_num
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,17004,unilab_send_msg_electrolyte_vol
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,17006,unilab_send_msg_assembly_type
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,17008,unilab_send_msg_assembly_pressure
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,16000,data_assembly_coin_cell_num
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,16002,data_open_circuit_voltage
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,16004,
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,16006,
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,16008,
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,16010,data_pole_weight
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,16012,data_assembly_time
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,16014,data_assembly_pressure
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,16016,data_electrolyte_volume
REG_DATA_COIN_NUM,INT16,,,,hold_register,16018,data_coin_num
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,16020,data_electrolyte_code()
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,16030,data_coin_cell_code()
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,18004,data_stack_vision_code()
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,16050,data_glove_box_pressure
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,16052,data_glove_box_water_content
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,16054,data_glove_box_o2_content
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9720,
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9520,
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,17496,
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,16000,
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,9730,
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,9530,
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,16018,ASSEMBLY_TYPE7or8
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,9340,
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,17440,
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,17450,
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,17480,
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,17443,
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,17453,
REG_MSG_PRESS_MODE,BOOL,,压制模式false:压力检测模式True:距离模式),,coil,9360,电池压制模式
,,,,,,,
,BOOL,,视觉对位false:使用true:忽略),,coil,9300,视觉对位
,BOOL,,复检false:使用true:忽略),,coil,9310,视觉复检
,BOOL,,手套箱_左仓false:使用true:忽略),,coil,9320,手套箱左仓
,BOOL,,手套箱_右仓false:使用true:忽略),,coil,9420,手套箱右仓
,BOOL,,真空检知false:使用true:忽略),,coil,9350,真空检知
,BOOL,,电解液添加模式false:单次滴液true:二次滴液),,coil,9370,滴液模式
,BOOL,,正极片称重false:使用true:忽略),,coil,9380,正极片称重
,BOOL,,正负极片组装方式false:正装true:倒装),,coil,9390,正负极反装
,BOOL,,压制清洁false:使用true:忽略),,coil,9400,压制清洁
,BOOL,,物料盘摆盘方式false:水平摆盘true:堆叠摆盘),,coil,9410,负极片摆盘方式
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁false:使用true:忽略),,coil,9460,
1 Name DataType InitValue Comment Attribute DeviceType Address
2 COIL_SYS_START_CMD BOOL coil 9010
3 COIL_SYS_STOP_CMD BOOL coil 9020
4 COIL_SYS_RESET_CMD BOOL coil 9030
5 COIL_SYS_HAND_CMD BOOL coil 9040
6 COIL_SYS_AUTO_CMD BOOL coil 9050
7 COIL_SYS_INIT_CMD BOOL coil 9060
8 COIL_UNILAB_SEND_MSG_SUCC_CMD BOOL coil 9700
9 COIL_UNILAB_REC_MSG_SUCC_CMD BOOL coil 9710 unilab_rec_msg_succ_cmd
10 COIL_SYS_START_STATUS BOOL coil 9210
11 COIL_SYS_STOP_STATUS BOOL coil 9220
12 COIL_SYS_RESET_STATUS BOOL coil 9230
13 COIL_SYS_HAND_STATUS BOOL coil 9240
14 COIL_SYS_AUTO_STATUS BOOL coil 9250
15 COIL_SYS_INIT_STATUS BOOL coil 9260
16 COIL_REQUEST_REC_MSG_STATUS BOOL coil 9500
17 COIL_REQUEST_SEND_MSG_STATUS BOOL coil 9510 request_send_msg_status
18 REG_MSG_ELECTROLYTE_USE_NUM INT16 hold_register 17000
19 REG_MSG_ELECTROLYTE_NUM INT16 hold_register 17002 unilab_send_msg_electrolyte_num
20 REG_MSG_ELECTROLYTE_VOLUME INT16 hold_register 17004 unilab_send_msg_electrolyte_vol
21 REG_MSG_ASSEMBLY_TYPE INT16 hold_register 17006 unilab_send_msg_assembly_type
22 REG_MSG_ASSEMBLY_PRESSURE INT16 hold_register 17008 unilab_send_msg_assembly_pressure
23 REG_DATA_ASSEMBLY_COIN_CELL_NUM INT16 hold_register 16000 data_assembly_coin_cell_num
24 REG_DATA_OPEN_CIRCUIT_VOLTAGE FLOAT32 hold_register 16002 data_open_circuit_voltage
25 REG_DATA_AXIS_X_POS FLOAT32 hold_register 16004
26 REG_DATA_AXIS_Y_POS FLOAT32 hold_register 16006
27 REG_DATA_AXIS_Z_POS FLOAT32 hold_register 16008
28 REG_DATA_POLE_WEIGHT FLOAT32 hold_register 16010 data_pole_weight
29 REG_DATA_ASSEMBLY_PER_TIME FLOAT32 hold_register 16012 data_assembly_time
30 REG_DATA_ASSEMBLY_PRESSURE INT16 hold_register 16014 data_assembly_pressure
31 REG_DATA_ELECTROLYTE_VOLUME INT16 hold_register 16016 data_electrolyte_volume
32 REG_DATA_COIN_NUM INT16 hold_register 16018 data_coin_num
33 REG_DATA_ELECTROLYTE_CODE STRING hold_register 16020 data_electrolyte_code()
34 REG_DATA_COIN_CELL_CODE STRING hold_register 16030 data_coin_cell_code()
35 REG_DATA_STACK_VISON_CODE STRING hold_register 18004 data_stack_vision_code()
36 REG_DATA_GLOVE_BOX_PRESSURE FLOAT32 hold_register 16050 data_glove_box_pressure
37 REG_DATA_GLOVE_BOX_WATER_CONTENT FLOAT32 hold_register 16052 data_glove_box_water_content
38 REG_DATA_GLOVE_BOX_O2_CONTENT FLOAT32 hold_register 16054 data_glove_box_o2_content
39 UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM BOOL coil 9720
40 UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM BOOL coil 9520
41 REG_MSG_ELECTROLYTE_NUM_USED INT16 hold_register 17496
42 REG_DATA_ELECTROLYTE_USE_NUM INT16 hold_register 16000
43 UNILAB_SEND_FINISHED_CMD BOOL coil 9730
44 UNILAB_RECE_FINISHED_CMD BOOL coil 9530
45 REG_DATA_ASSEMBLY_TYPE INT16 hold_register 16018 ASSEMBLY_TYPE7or8
46 COIL_ALUMINUM_FOIL BOOL 使用铝箔垫 coil 9340
47 REG_MSG_NE_PLATE_MATRIX INT16 负极片矩阵点位 hold_register 17440
48 REG_MSG_SEPARATOR_PLATE_MATRIX INT16 隔膜矩阵点位 hold_register 17450
49 REG_MSG_TIP_BOX_MATRIX INT16 移液枪头矩阵点位 hold_register 17480
50 REG_MSG_NE_PLATE_NUM INT16 负极片盘数 hold_register 17443
51 REG_MSG_SEPARATOR_PLATE_NUM INT16 隔膜盘数 hold_register 17453
52 REG_MSG_PRESS_MODE BOOL 压制模式(false:压力检测模式,True:距离模式) coil 9360 电池压制模式
53
54 BOOL 视觉对位(false:使用,true:忽略) coil 9300 视觉对位
55 BOOL 复检(false:使用,true:忽略) coil 9310 视觉复检
56 BOOL 手套箱_左仓(false:使用,true:忽略) coil 9320 手套箱左仓
57 BOOL 手套箱_右仓(false:使用,true:忽略) coil 9420 手套箱右仓
58 BOOL 真空检知(false:使用,true:忽略) coil 9350 真空检知
59 BOOL 电解液添加模式(false:单次滴液,true:二次滴液) coil 9370 滴液模式
60 BOOL 正极片称重(false:使用,true:忽略) coil 9380 正极片称重
61 BOOL 正负极片组装方式(false:正装,true:倒装) coil 9390 正负极反装
62 BOOL 压制清洁(false:使用,true:忽略) coil 9400 压制清洁
63 BOOL 物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘) coil 9410 负极片摆盘方式
64 REG_MSG_BATTERY_CLEAN_IGNORE BOOL 忽略电池清洁(false:使用,true:忽略) coil 9460

View File

@@ -0,0 +1,64 @@
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
COIL_SYS_START_CMD,BOOL,,,,coil,8010,
COIL_SYS_STOP_CMD,BOOL,,,,coil,8020,
COIL_SYS_RESET_CMD,BOOL,,,,coil,8030,
COIL_SYS_HAND_CMD,BOOL,,,,coil,8040,
COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050,
COIL_SYS_INIT_CMD,BOOL,,,,coil,8060,
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700,
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd
COIL_SYS_START_STATUS,BOOL,,,,coil,8210,
COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220,
COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230,
COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240,
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250,
COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260,
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500,
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000,
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004,
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006,
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008,
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume
REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code()
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code()
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code()
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720,
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520,
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496,
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340,
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,440,
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,450,
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,480,
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,443,
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,453,
REG_MSG_PRESS_MODE,BOOL,,压制模式false:压力检测模式True:距离模式),,coil,8360,电池压制模式
,,,,,,,
,BOOL,,视觉对位false:使用true:忽略),,coil,8300,视觉对位
,BOOL,,复检false:使用true:忽略),,coil,8310,视觉复检
,BOOL,,手套箱_左仓false:使用true:忽略),,coil,8320,手套箱左仓
,BOOL,,手套箱_右仓false:使用true:忽略),,coil,8420,手套箱右仓
,BOOL,,真空检知false:使用true:忽略),,coil,8350,真空检知
,BOOL,,电解液添加模式false:单次滴液true:二次滴液),,coil,8370,滴液模式
,BOOL,,正极片称重false:使用true:忽略),,coil,8380,正极片称重
,BOOL,,正负极片组装方式false:正装true:倒装),,coil,8390,正负极反装
,BOOL,,压制清洁false:使用true:忽略),,coil,8400,压制清洁
,BOOL,,物料盘摆盘方式false:水平摆盘true:堆叠摆盘),,coil,8410,负极片摆盘方式
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁false:使用true:忽略),,coil,8460,
1 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 COIL_ALUMINUM_FOIL BOOL 使用铝箔垫 coil 8340
47 REG_MSG_NE_PLATE_MATRIX INT16 负极片矩阵点位 hold_register 440
48 REG_MSG_SEPARATOR_PLATE_MATRIX INT16 隔膜矩阵点位 hold_register 450
49 REG_MSG_TIP_BOX_MATRIX INT16 移液枪头矩阵点位 hold_register 480
50 REG_MSG_NE_PLATE_NUM INT16 负极片盘数 hold_register 443
51 REG_MSG_SEPARATOR_PLATE_NUM INT16 隔膜盘数 hold_register 453
52 REG_MSG_PRESS_MODE BOOL 压制模式(false:压力检测模式,True:距离模式) coil 8360 电池压制模式
53
54 BOOL 视觉对位(false:使用,true:忽略) coil 8300 视觉对位
55 BOOL 复检(false:使用,true:忽略) coil 8310 视觉复检
56 BOOL 手套箱_左仓(false:使用,true:忽略) coil 8320 手套箱左仓
57 BOOL 手套箱_右仓(false:使用,true:忽略) coil 8420 手套箱右仓
58 BOOL 真空检知(false:使用,true:忽略) coil 8350 真空检知
59 BOOL 电解液添加模式(false:单次滴液,true:二次滴液) coil 8370 滴液模式
60 BOOL 正极片称重(false:使用,true:忽略) coil 8380 正极片称重
61 BOOL 正负极片组装方式(false:正装,true:倒装) coil 8390 正负极反装
62 BOOL 压制清洁(false:使用,true:忽略) coil 8400 压制清洁
63 BOOL 物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘) coil 8410 负极片摆盘方式
64 REG_MSG_BATTERY_CLEAN_IGNORE BOOL 忽略电池清洁(false:使用,true:忽略) coil 8460

View File

@@ -0,0 +1,39 @@
{
"nodes": [
{
"id": "bioyond_cell_workstation",
"name": "配液分液工站",
"children": [
],
"parent": null,
"type": "device",
"class": "bioyond_cell",
"config": {
"protocol_type": [],
"station_resource": {}
},
"data": {}
},
{
"id": "BatteryStation",
"name": "扣电工作站",
"children": [
"coin_cell_deck"
],
"parent": null,
"type": "device",
"class": "coincellassemblyworkstation_device",
"position": {
"x": -600,
"y": -400,
"z": 0
},
"config": {
"debug_mode": false,
"protocol_type": []
}
}
],
"links": []
}

View File

@@ -668,7 +668,7 @@ __all__ = [
if __name__ == "__main__":
# 简单测试HTTP服务
class BioyondWorkstation:
class DummyWorkstation:
device_id = "WS-001"
def process_step_finish_report(self, report_request):

View File

@@ -1,26 +0,0 @@
{
"nodes": [
{
"id": "XRD_D7MATE_STATION",
"name": "XRD_D7MATE",
"parent": null,
"type": "device",
"class": "xrd_d7mate",
"position": {
"x": 720.0,
"y": 200.0,
"z": 0
},
"config": {
"host": "127.0.0.1",
"port": 6001,
"timeout": 10.0
},
"data": {
"input_hint": "start 支持单字符串输入:'sample_name 样品A start_theta 10.0 end_theta 80.0 increment 0.02 exp_time 0.1 [wait_minutes 3]';也支持等号形式 'sample_id=样品A start_theta=10.0 end_theta=80.0 increment=0.02 exp_time=0.1 wait_minutes=3'"
},
"children": []
}
],
"links": []
}

View File

@@ -1,939 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
XRD D7-Mate设备驱动
支持XRD D7-Mate设备的TCP通信协议包括自动模式控制、上样流程、数据获取、下样流程和高压电源控制等功能。
通信协议版本1.0.0
"""
import json
import socket
import struct
import time
from typing import Dict, List, Optional, Tuple, Any, Union
class XRDClient:
def __init__(self, host='127.0.0.1', port=6001, timeout=10.0):
"""
初始化XRD D7-Mate客户端
Args:
host (str): 设备IP地址
port (int): 通信端口默认6001
timeout (float): 超时时间,单位秒
"""
self.host = host
self.port = port
self.timeout = timeout
self.sock = None
self._ros_node = None # ROS节点引用由框架设置
def post_init(self, ros_node):
"""
ROS节点初始化后的回调方法保存ROS节点引用但不自动连接
Args:
ros_node: ROS节点实例
"""
self._ros_node = ros_node
ros_node.lab_logger().info(f"XRD D7-Mate设备已初始化将在需要时连接: {self.host}:{self.port}")
# 不自动连接,只有在调用具体功能时才建立连接
def connect(self):
"""
建立TCP连接到XRD D7-Mate设备
Raises:
ConnectionError: 连接失败时抛出
"""
try:
self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
self.sock.settimeout(self.timeout)
except Exception as e:
raise ConnectionError(f"Failed to connect to {self.host}:{self.port} - {str(e)}")
def close(self):
"""
关闭与XRD D7-Mate设备的TCP连接
"""
if self.sock:
try:
self.sock.close()
except Exception:
pass # 忽略关闭时的错误
finally:
self.sock = None
def _ensure_connection(self) -> bool:
"""
确保连接存在,如果不存在则尝试建立连接
Returns:
bool: 连接是否成功建立
"""
if self.sock is None:
try:
self.connect()
return True
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"建立连接失败: {e}")
return False
return True
def _receive_with_length_prefix(self) -> dict:
"""
使用长度前缀协议接收数据
Returns:
dict: 解析后的JSON响应数据
Raises:
ConnectionError: 连接错误
TimeoutError: 超时错误
"""
try:
# 首先接收4字节的长度信息
length_data = bytearray()
while len(length_data) < 4:
chunk = self.sock.recv(4 - len(length_data))
if not chunk:
raise ConnectionError("Connection closed while receiving length prefix")
length_data.extend(chunk)
# 解析长度(大端序无符号整数)
data_length = struct.unpack('>I', length_data)[0]
if self._ros_node:
self._ros_node.lab_logger().info(f"接收到数据长度: {data_length} 字节")
# 根据长度接收实际数据
json_data = bytearray()
while len(json_data) < data_length:
remaining = data_length - len(json_data)
chunk = self.sock.recv(min(4096, remaining))
if not chunk:
raise ConnectionError("Connection closed while receiving JSON data")
json_data.extend(chunk)
# 解码JSON数据优先使用UTF-8失败时尝试GBK
try:
json_str = json_data.decode('utf-8')
except UnicodeDecodeError:
json_str = json_data.decode('gbk')
# 解析JSON
result = json.loads(json_str)
if self._ros_node:
self._ros_node.lab_logger().info(f"成功解析JSON响应: {result}")
return result
except socket.timeout:
if self._ros_node:
self._ros_node.lab_logger().warning(f"接收超时")
raise TimeoutError(f"recv() timed out after {self.timeout:.1f}s")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"接收数据失败: {e}")
raise ConnectionError(f"Failed to receive data: {str(e)}")
def _send_command(self, cmd: dict) -> dict:
"""
使用长度前缀协议发送命令到XRD D7-Mate设备并接收响应
Args:
cmd (dict): 要发送的命令字典
Returns:
dict: 设备响应的JSON数据
Raises:
ConnectionError: 连接错误
TimeoutError: 超时错误
"""
# 确保连接存在,如果不存在则建立连接
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info(f"为命令重新建立连接")
except Exception as e:
raise ConnectionError(f"Failed to establish connection: {str(e)}")
try:
# 序列化命令为JSON
json_str = json.dumps(cmd, ensure_ascii=False)
payload = json_str.encode('utf-8')
# 计算JSON数据长度并打包为4字节大端序无符号整数
length_prefix = struct.pack('>I', len(payload))
if self._ros_node:
self._ros_node.lab_logger().info(f"发送JSON命令到XRD D7-Mate: {json_str}")
self._ros_node.lab_logger().info(f"发送数据长度: {len(payload)} 字节")
# 发送长度前缀
self.sock.sendall(length_prefix)
# 发送JSON数据
self.sock.sendall(payload)
# 使用长度前缀协议接收响应
response = self._receive_with_length_prefix()
return response
except Exception as e:
# 如果是连接错误,尝试重新连接一次
if "远程主机强迫关闭了一个现有的连接" in str(e) or "10054" in str(e):
if self._ros_node:
self._ros_node.lab_logger().warning(f"连接被远程主机关闭,尝试重新连接: {e}")
try:
self.close()
self.connect()
# 重新发送命令
json_str = json.dumps(cmd, ensure_ascii=False)
payload = json_str.encode('utf-8')
if self._ros_node:
self._ros_node.lab_logger().info(f"重新发送JSON命令到XRD D7-Mate: {json_str}")
self.sock.sendall(payload)
# 重新接收响应
buffer = bytearray()
start = time.time()
while True:
try:
chunk = self.sock.recv(4096)
if not chunk:
break
buffer.extend(chunk)
# 尝试解码和解析JSON
try:
text = buffer.decode('utf-8', errors='strict')
text = text.strip()
if text.startswith('{'):
brace_count = 0
json_end = -1
for i, char in enumerate(text):
if char == '{':
brace_count += 1
elif char == '}':
brace_count -= 1
if brace_count == 0:
json_end = i + 1
break
if json_end > 0:
text = text[:json_end]
result = json.loads(text)
if self._ros_node:
self._ros_node.lab_logger().info(f"重连后成功解析JSON响应: {result}")
return result
except (UnicodeDecodeError, json.JSONDecodeError):
pass
except socket.timeout:
if self._ros_node:
self._ros_node.lab_logger().warning(f"重连后接收超时")
raise TimeoutError(f"recv() timed out after reconnection")
if time.time() - start > self.timeout * 2:
raise TimeoutError(f"No complete JSON received after reconnection")
except Exception as retry_e:
if self._ros_node:
self._ros_node.lab_logger().error(f"重连失败: {retry_e}")
raise ConnectionError(f"Connection retry failed: {str(retry_e)}")
if isinstance(e, (ConnectionError, TimeoutError)):
raise
else:
raise ConnectionError(f"Command send failed: {str(e)}")
# ==================== 自动模式控制 ====================
def start_auto_mode(self, status: bool) -> dict:
"""
启动或停止自动模式
Args:
status (bool): True-启动自动模式False-停止自动模式
Returns:
dict: 响应结果包含status、timestamp、message
"""
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
# 按协议要求content 直接为布尔值使用传入的status参数
cmd = {
"command": "START_AUTO_MODE",
"content": {
"status": bool(True)
}
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送自动模式控制命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到自动模式控制响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"自动模式控制失败: {e}")
return {"status": False, "message": f"自动模式控制失败: {str(e)}"}
# ==================== 上样流程 ====================
def get_sample_request(self) -> dict:
"""
上样请求,检查是否允许上样
Returns:
dict: 响应结果包含status、timestamp、message
"""
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
cmd = {
"command": "GET_SAMPLE_REQUEST",
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送上样请求命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到上样请求响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"上样请求失败: {e}")
return {"status": False, "message": f"上样请求失败: {str(e)}"}
def send_sample_ready(self, sample_id: str, start_theta: float, end_theta: float,
increment: float, exp_time: float) -> dict:
"""
送样完成后,发送样品信息和采集参数
Args:
sample_id (str): 样品标识符
start_theta (float): 起始角度≥5°
end_theta (float): 结束角度≥5.5°且必须大于start_theta
increment (float): 角度增量≥0.005
exp_time (float): 曝光时间0.1-5.0秒)
Returns:
dict: 响应结果包含status、timestamp、message等
"""
# 参数验证
if start_theta < 5.0:
return {"status": False, "message": "起始角度必须≥5°"}
if end_theta < 5.5:
return {"status": False, "message": "结束角度必须≥5.5°"}
if end_theta <= start_theta:
return {"status": False, "message": "结束角度必须大于起始角度"}
if increment < 0.005:
return {"status": False, "message": "角度增量必须≥0.005"}
if not (0.1 <= exp_time <= 5.0):
return {"status": False, "message": "曝光时间必须在0.1-5.0秒之间"}
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
cmd = {
"command": "SEND_SAMPLE_READY",
"content": {
"sample_id": sample_id,
"start_theta": start_theta,
"end_theta": end_theta,
"increment": increment,
"exp_time": exp_time
}
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送样品准备完成命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到样品准备完成响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"样品准备完成失败: {e}")
return {"status": False, "message": f"样品准备完成失败: {str(e)}"}
# ==================== 数据获取 ====================
def get_current_acquire_data(self) -> dict:
"""
获取当前正在采集的样品数据
Returns:
dict: 响应结果包含status、timestamp、sample_id、Energy、Intensity等
"""
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
cmd = {
"command": "GET_CURRENT_ACQUIRE_DATA",
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送获取采集数据命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到获取采集数据响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"获取采集数据失败: {e}")
return {"status": False, "message": f"获取采集数据失败: {str(e)}"}
def get_sample_status(self) -> dict:
"""
获取工位样品状态及设备状态
Returns:
dict: 响应结果包含status、timestamp、Station等
"""
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
cmd = {
"command": "GET_SAMPLE_STATUS",
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送获取样品状态命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到获取样品状态响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"获取样品状态失败: {e}")
return {"status": False, "message": f"获取样品状态失败: {str(e)}"}
# ==================== 下样流程 ====================
def get_sample_down(self, sample_station: int) -> dict:
"""
下样请求
Args:
sample_station (int): 下样工位1, 2, 3
Returns:
dict: 响应结果包含status、timestamp、sample_info等
"""
# 参数验证
if sample_station not in [1, 2, 3]:
return {"status": False, "message": "下样工位必须是1、2或3"}
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
# 按协议要求content 直接为整数工位号
cmd = {
"command": "GET_SAMPLE_DOWN",
"content": {
"Sample station":int(3)
}
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送下样请求命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到下样请求响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"下样请求失败: {e}")
return {"status": False, "message": f"下样请求失败: {str(e)}"}
def send_sample_down_ready(self) -> dict:
"""
下样完成命令
Returns:
dict: 响应结果包含status、timestamp、message
"""
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
cmd = {
"command": "SEND_SAMPLE_DOWN_READY",
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送下样完成命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到下样完成响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"下样完成失败: {e}")
return {"status": False, "message": f"下样完成失败: {str(e)}"}
# ==================== 高压电源控制 ====================
def set_power_on(self) -> dict:
"""
高压电源开启
Returns:
dict: 响应结果包含status、timestamp、message
"""
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
cmd = {
"command": "SET_POWER_ON",
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送高压电源开启命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到高压开启响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"高压开启失败: {e}")
return {"status": False, "message": f"高压开启失败: {str(e)}"}
def set_power_off(self) -> dict:
"""
高压电源关闭
Returns:
dict: 响应结果包含status、timestamp、message
"""
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
cmd = {
"command": "SET_POWER_OFF",
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送高压电源关闭命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到高压关闭响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"高压关闭失败: {e}")
return {"status": False, "message": f"高压关闭失败: {str(e)}"}
def set_voltage_current(self, voltage: float, current: float) -> dict:
"""
设置高压电源电压和电流
Args:
voltage (float): 电压值kV
current (float): 电流值mA
Returns:
dict: 响应结果包含status、timestamp、message
"""
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备重新连接成功")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"XRD D7-Mate设备连接失败: {e}")
return {"status": False, "message": "设备连接异常"}
try:
cmd = {
"command": "SET_VOLTAGE_CURRENT",
"content": {
"voltage": voltage,
"current": current
}
}
if self._ros_node:
self._ros_node.lab_logger().info(f"发送设置电压电流命令: {cmd}")
response = self._send_command(cmd)
if self._ros_node:
self._ros_node.lab_logger().info(f"收到设置电压电流响应: {response}")
return response
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"设置电压电流失败: {e}")
return {"status": False, "message": f"设置电压电流失败: {str(e)}"}
def start(self, sample_id: str = "", start_theta: float = 10.0, end_theta: float = 80.0,
increment: float = 0.05, exp_time: float = 0.1, wait_minutes: float = 3.0,
string: str = "") -> dict:
"""
Start 主流程:
1) 启动自动模式;
2) 发送上样请求并等待允许;
3) 等待指定分钟后发送样品准备完成(携带采集参数);
4) 周期性轮询采集数据与工位状态;
5) 一旦任一下样位变为 True执行下样流程GET_SAMPLE_DOWN + SEND_SAMPLE_DOWN_READY
Args:
sample_id: 样品名称
start_theta: 起始角度≥5°
end_theta: 结束角度≥5.5°,且必须大于 start_theta
increment: 角度增量≥0.005
exp_time: 曝光时间0.1-5.0 秒)
wait_minutes: 在允许上样后、发送样品准备完成前的等待分钟数(默认 3 分钟)
string: 字符串格式的参数输入,如果提供则优先解析使用
Returns:
dict: {"return_info": str, "success": bool}
"""
try:
# 强制类型转换:除 sample_id 外的所有输入均转换为 float若为字符串
def _to_float(v, default):
try:
return float(v)
except (TypeError, ValueError):
return float(default)
if not isinstance(sample_id, str):
sample_id = str(sample_id)
if isinstance(start_theta, str):
start_theta = _to_float(start_theta, 10.0)
if isinstance(end_theta, str):
end_theta = _to_float(end_theta, 80.0)
if isinstance(increment, str):
increment = _to_float(increment, 0.05)
if isinstance(exp_time, str):
exp_time = _to_float(exp_time, 0.1)
if isinstance(wait_minutes, str):
wait_minutes = _to_float(wait_minutes, 3.0)
# 不再从 string 参数解析覆盖;保留参数但忽略字符串解析,统一使用结构化输入
# 确保设备连接
if not self.sock:
try:
self.connect()
if self._ros_node:
self._ros_node.lab_logger().info("XRD D7-Mate设备连接成功开始执行start流程")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"XRD D7-Mate设备连接失败: {e}")
return {"return_info": f"设备连接失败: {str(e)}", "success": False}
# 1) 启动自动模式
r_auto = self.start_auto_mode(True)
if not r_auto.get("status", False):
return {"return_info": f"启动自动模式失败: {r_auto.get('message', '未知')}", "success": False}
if self._ros_node:
self._ros_node.lab_logger().info(f"自动模式已启动: {r_auto}")
# 2) 上样请求
r_req = self.get_sample_request()
if not r_req.get("status", False):
return {"return_info": f"上样请求未允许: {r_req.get('message', '未知')}", "success": False}
if self._ros_node:
self._ros_node.lab_logger().info(f"上样已允许: {r_req}")
# 3) 等待指定分钟后发送样品准备完成
wait_seconds = max(0.0, float(wait_minutes)) * 60.0
if self._ros_node:
self._ros_node.lab_logger().info(f"等待 {wait_minutes} 分钟后发送样品准备完成")
time.sleep(wait_seconds)
r_ready = self.send_sample_ready(sample_id=sample_id,
start_theta=start_theta,
end_theta=end_theta,
increment=increment,
exp_time=exp_time)
if not r_ready.get("status", False):
return {"return_info": f"样品准备完成失败: {r_ready.get('message', '未知')}", "success": False}
if self._ros_node:
self._ros_node.lab_logger().info(f"样品准备完成已发送: {r_ready}")
# 4) 轮询采集数据与工位状态
polling_interval = 5.0 # 秒
down_station_idx: Optional[int] = None
while True:
try:
r_data = self.get_current_acquire_data()
if self._ros_node:
self._ros_node.lab_logger().info(f"采集中数据: {r_data}")
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"获取采集数据失败: {e}")
try:
r_status = self.get_sample_status()
if self._ros_node:
self._ros_node.lab_logger().info(f"工位状态: {r_status}")
station = r_status.get("Station", {})
if isinstance(station, dict):
for idx in (1, 2, 3):
key = f"DownStation{idx}"
val = station.get(key)
if isinstance(val, bool) and val:
down_station_idx = idx
break
if down_station_idx is not None:
break
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"获取工位状态失败: {e}")
time.sleep(polling_interval)
if down_station_idx is None:
return {"return_info": "未检测到任一下样位 True流程未完成", "success": False}
# 5) 下样流程
r_down = self.get_sample_down(down_station_idx)
if not r_down.get("status", False):
return {"return_info": f"下样请求失败(工位 {down_station_idx}): {r_down.get('message', '未知')}", "success": False}
if self._ros_node:
self._ros_node.lab_logger().info(f"下样请求成功(工位 {down_station_idx}): {r_down}")
r_ready_down = self.send_sample_down_ready()
if not r_ready_down.get("status", False):
return {"return_info": f"下样完成发送失败: {r_ready_down.get('message', '未知')}", "success": False}
if self._ros_node:
self._ros_node.lab_logger().info(f"下样完成已发送: {r_ready_down}")
return {"return_info": f"Start流程完成工位 {down_station_idx} 已下样", "success": True}
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().error(f"Start流程异常: {e}")
return {"return_info": f"Start流程异常: {str(e)}", "success": False}
def _parse_start_params(self, params: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
"""
解析UI输入参数为 Start 流程参数。
- 从UI字典中读取各个字段的字符串值
- 将数值字段从字符串转换为 float 类型
- 保留 sample_id 为字符串类型
返回:
dict: {sample_id, start_theta, end_theta, increment, exp_time, wait_minutes}
"""
# 如果传入为字典,则直接按键读取;否则给出警告并使用空字典
if isinstance(params, dict):
p = params
else:
p = {}
if self._ros_node:
self._ros_node.lab_logger().warning("start 参数应为结构化字典")
def _to_float(v, default):
"""将UI输入的字符串值转换为float处理空值和无效值"""
if v is None or v == '':
return float(default)
try:
# 处理字符串输入来自UI
if isinstance(v, str):
v = v.strip()
if v == '':
return float(default)
return float(v)
except (TypeError, ValueError):
return float(default)
# 从UI输入字典中读取参数
sample_id = p.get('sample_id') or p.get('sample_name') or '样品名称'
if not isinstance(sample_id, str):
sample_id = str(sample_id)
# 将UI字符串输入转换为float
result: Dict[str, Any] = {
'sample_id': sample_id,
'start_theta': _to_float(p.get('start_theta'), 10.0),
'end_theta': _to_float(p.get('end_theta'), 80.0),
'increment': _to_float(p.get('increment'), 0.05),
'exp_time': _to_float(p.get('exp_time'), 0.1),
'wait_minutes': _to_float(p.get('wait_minutes'), 3.0),
}
return result
def start_from_string(self, params: Union[str, Dict[str, Any]]) -> dict:
"""
从UI输入参数执行 Start 主流程。
接收来自用户界面的参数字典,其中数值字段为字符串格式,自动转换为正确的类型。
参数:
params: UI输入参数字典例如:
{
'sample_id': 'teste',
'start_theta': '10.0', # UI字符串输入
'end_theta': '25.0', # UI字符串输入
'increment': '0.05', # UI字符串输入
'exp_time': '0.10', # UI字符串输入
'wait_minutes': '0.5' # UI字符串输入
}
返回:
dict: 执行结果
"""
parsed = self._parse_start_params(params)
sample_id = parsed.get('sample_id', '样品名称')
start_theta = float(parsed.get('start_theta', 10.0))
end_theta = float(parsed.get('end_theta', 80.0))
increment = float(parsed.get('increment', 0.05))
exp_time = float(parsed.get('exp_time', 0.1))
wait_minutes = float(parsed.get('wait_minutes', 3.0))
return self.start(
sample_id=sample_id,
start_theta=start_theta,
end_theta=end_theta,
increment=increment,
exp_time=exp_time,
wait_minutes=wait_minutes,
)
# 测试函数
def test_xrd_client():
"""
测试XRD客户端功能
"""
client = XRDClient(host='127.0.0.1', port=6001)
try:
# 测试连接
client.connect()
print("连接成功")
# 测试启动自动模式
result = client.start_auto_mode(True)
print(f"启动自动模式: {result}")
# 测试上样请求
result = client.get_sample_request()
print(f"上样请求: {result}")
# 测试获取样品状态
result = client.get_sample_status()
print(f"样品状态: {result}")
# 测试高压开启
result = client.set_power_on()
print(f"高压开启: {result}")
except Exception as e:
print(f"测试失败: {e}")
finally:
client.close()
if __name__ == "__main__":
test_xrd_client()
# 为了兼容性,提供别名
XRD_D7Mate = XRDClient

View File

@@ -1,252 +0,0 @@
workstation.bioyond_dispensing_station:
category:
- workstation
- bioyond
class:
action_value_mappings:
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
required:
- config
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -1,404 +0,0 @@
bioyond_dispensing_station:
category:
- workstation
- bioyond
- bioyond_dispensing_station
class:
action_value_mappings:
batch_create_90_10_vial_feeding_tasks:
feedback: {}
goal:
delay_time: delay_time
hold_m_name: hold_m_name
liquid_material_name: liquid_material_name
speed: speed
temperature: temperature
titration: titration
goal_default:
delay_time: '600'
hold_m_name: ''
liquid_material_name: NMP
speed: '400'
temperature: '40'
titration: ''
handles:
input:
- data_key: titration
data_source: handle
data_type: object
handler_key: titration
io_type: source
label: Titration Data From Calculation Node
result:
return_info: return_info
schema:
description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。
properties:
feedback:
properties: {}
required: []
title: BatchCreate9010VialFeedingTasks_Feedback
type: object
goal:
properties:
delay_time:
default: '600'
description: 延迟时间(秒),默认600
type: string
hold_m_name:
description: 库位名称,如"C01",必填参数
type: string
liquid_material_name:
default: NMP
description: 10%物料的液体物料名称,默认为"NMP"
type: string
speed:
default: '400'
description: 搅拌速度,默认400
type: string
temperature:
default: '40'
description: 温度(℃),默认40
type: string
titration:
description: '滴定信息对象,包含: name(物料名称), main_portion(主称固体质量g), titration_portion(滴定固体质量g),
titration_solvent(滴定溶液体积mL)'
type: string
required:
- titration
- hold_m_name
title: BatchCreate9010VialFeedingTasks_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: BatchCreate9010VialFeedingTasks_Result
type: object
required:
- goal
title: BatchCreate9010VialFeedingTasks
type: object
type: UniLabJsonCommand
batch_create_diamine_solution_tasks:
feedback: {}
goal:
delay_time: delay_time
liquid_material_name: liquid_material_name
solutions: solutions
speed: speed
temperature: temperature
goal_default:
delay_time: '600'
liquid_material_name: NMP
solutions: ''
speed: '400'
temperature: '20'
handles:
input:
- data_key: solutions
data_source: handle
data_type: array
handler_key: solutions
io_type: source
label: Solution Data From Python
result:
return_info: return_info
schema:
description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务,每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。
properties:
feedback:
properties: {}
required: []
title: BatchCreateDiamineSolutionTasks_Feedback
type: object
goal:
properties:
delay_time:
default: '600'
description: 溶液配置完成后的延迟时间用于充分混合和溶解默认600秒
type: string
liquid_material_name:
default: NMP
description: 液体溶剂名称用于溶解固体物料默认为NMPN-甲基吡咯烷酮)
type: string
solutions:
description: '溶液列表JSON数组格式每个元素包含: name(物料名称), order(序号), solid_mass(固体质量g),
solvent_volume(溶剂体积mL)。示例: [{"name": "MDA", "order": 0, "solid_mass":
5.0, "solvent_volume": 20}, {"name": "MPDA", "order": 1, "solid_mass":
4.5, "solvent_volume": 18}]'
type: string
speed:
default: '400'
description: 搅拌速度rpm用于混合溶液默认400转/分钟
type: string
temperature:
default: '20'
description: 配置温度溶液配置过程的目标温度默认20℃室温
type: string
required:
- solutions
title: BatchCreateDiamineSolutionTasks_Goal
type: object
result:
properties:
return_info:
description: 批量任务创建结果汇总JSON格式包含总数、成功数、失败数及每个任务的详细信息
type: string
required:
- return_info
title: BatchCreateDiamineSolutionTasks_Result
type: object
required:
- goal
title: BatchCreateDiamineSolutionTasks
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: preparation_station.webp
init_param_schema:
config:
properties:
config:
type: string
required:
- config
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -1,231 +1,3 @@
hplc.agilent:
category:
- characterization_chromatic
class:
action_value_mappings:
auto-check_status:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态确保系统稳定运行及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: check_status参数
type: object
type: UniLabJsonCommand
auto-extract_data_from_txt:
feedback: {}
goal: {}
goal_default:
file_path: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
properties:
feedback: {}
goal:
properties:
file_path:
type: string
required:
- file_path
type: object
result: {}
required:
- goal
title: extract_data_from_txt参数
type: object
type: UniLabJsonCommand
auto-start_sequence:
feedback: {}
goal: {}
goal_default:
params: null
resource: null
wf_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
properties:
feedback: {}
goal:
properties:
params:
type: string
resource:
type: object
wf_name:
type: string
required:
- wf_name
type: object
result: {}
required:
- goal
title: start_sequence参数
type: object
type: UniLabJsonCommand
auto-try_close_sub_device:
feedback: {}
goal: {}
goal_default:
device_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
properties:
feedback: {}
goal:
properties:
device_name:
type: string
required: []
type: object
result: {}
required:
- goal
title: try_close_sub_device参数
type: object
type: UniLabJsonCommand
auto-try_open_sub_device:
feedback: {}
goal: {}
goal_default:
device_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块建立设备通信并进行自检。该函数提供连接验证和错误恢复机制确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
properties:
feedback: {}
goal:
properties:
device_name:
type: string
required: []
type: object
result: {}
required:
- goal
title: try_open_sub_device参数
type: object
type: UniLabJsonCommand
execute_command_from_outer:
feedback: {}
goal:
command: command
goal_default:
command: ''
handles: {}
result:
success: success
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: SendCmd_Feedback
type: object
goal:
properties:
command:
type: string
required:
- command
title: SendCmd_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SendCmd_Result
type: object
required:
- goal
title: SendCmd
type: object
type: SendCmd
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
status_types:
could_run: bool
data_file: String
device_status: str
driver_init_ok: bool
finish_status: str
is_running: bool
status_text: str
success: bool
type: python
config_info: []
description: 安捷伦高效液相色谱HPLC分析设备用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
handles: []
icon: ''
init_param_schema:
config:
properties:
driver_debug:
default: false
type: string
required: []
type: object
data:
properties:
could_run:
type: boolean
data_file:
items:
type: string
type: array
device_status:
type: string
driver_init_ok:
type: boolean
finish_status:
type: string
is_running:
type: boolean
status_text:
type: string
success:
type: boolean
required:
- status_text
- device_status
- could_run
- driver_init_ok
- is_running
- success
- finish_status
- data_file
type: object
version: 1.0.0
hplc.agilent-zhida:
category:
- characterization_chromatic

View File

@@ -1,194 +1 @@
raman.home_made:
category:
- characterization_optic
class:
action_value_mappings:
auto-ccd_time:
feedback: {}
goal: {}
goal_default:
int_time: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 设置CCD检测器积分时间的函数。用于配置拉曼光谱仪的信号采集时间控制光谱数据的质量和信噪比。较长的积分时间可获得更高的信号强度和更好的光谱质量但会增加测量时间。该函数允许根据样品特性和测量要求动态调整检测参数优化测量效果。
properties:
feedback: {}
goal:
properties:
int_time:
type: string
required:
- int_time
type: object
result: {}
required:
- goal
title: ccd_time参数
type: object
type: UniLabJsonCommand
auto-laser_on_power:
feedback: {}
goal: {}
goal_default:
output_voltage_laser: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 设置激光器输出功率的函数。用于控制拉曼光谱仪激光器的功率输出,调节激光强度以适应不同样品的测量需求。适当的激光功率能够获得良好的拉曼信号同时避免样品损伤。该函数支持精确的功率控制,确保测量结果的稳定性和重现性。
properties:
feedback: {}
goal:
properties:
output_voltage_laser:
type: string
required:
- output_voltage_laser
type: object
result: {}
required:
- goal
title: laser_on_power参数
type: object
type: UniLabJsonCommand
auto-raman_without_background:
feedback: {}
goal: {}
goal_default:
int_time: null
laser_power: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 执行无背景扣除的拉曼光谱测量函数。用于直接采集样品的拉曼光谱信号,不进行背景校正处理。该函数配置积分时间和激光功率参数,获取原始光谱数据用于后续的数据处理分析。适用于对光谱数据质量要求较高或需要自定义背景处理流程的测量场景。
properties:
feedback: {}
goal:
properties:
int_time:
type: string
laser_power:
type: string
required:
- int_time
- laser_power
type: object
result: {}
required:
- goal
title: raman_without_background参数
type: object
type: UniLabJsonCommand
auto-raman_without_background_average:
feedback: {}
goal: {}
goal_default:
average: null
int_time: null
laser_power: null
sample_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 执行多次平均的无背景拉曼光谱测量函数。通过多次测量取平均值来提高光谱数据的信噪比和测量精度,减少随机噪声影响。该函数支持自定义平均次数、积分时间、激光功率等参数,并可为样品指定名称便于数据管理。适用于对测量精度要求较高的定量分析和研究应用。
properties:
feedback: {}
goal:
properties:
average:
type: string
int_time:
type: string
laser_power:
type: string
sample_name:
type: string
required:
- sample_name
- int_time
- laser_power
- average
type: object
result: {}
required:
- goal
title: raman_without_background_average参数
type: object
type: UniLabJsonCommand
raman_cmd:
feedback: {}
goal:
command: command
goal_default:
command: ''
handles: {}
result:
success: success
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: SendCmd_Feedback
type: object
goal:
properties:
command:
type: string
required:
- command
title: SendCmd_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SendCmd_Result
type: object
required:
- goal
title: SendCmd
type: object
type: SendCmd
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
status_types: {}
type: python
config_info: []
description: 拉曼光谱分析设备用于物质的分子结构和化学成分表征。该设备集成激光器和CCD检测器通过串口通信控制激光功率和光谱采集。具备背景扣除、多次平均、自动数据处理等功能支持高精度的拉曼光谱测量。适用于材料表征、化学分析、质量控制、研究开发等需要分子指纹识别和结构分析的实验应用。
handles: []
icon: ''
init_param_schema:
config:
properties:
baudrate_ccd:
default: 921600
type: string
baudrate_laser:
default: 9600
type: string
port_ccd:
type: string
port_laser:
type: string
required:
- port_laser
- port_ccd
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0
{}

View File

@@ -0,0 +1,586 @@
coincellassemblyworkstation_device:
category:
- coin_cell_workstation
class:
action_value_mappings:
auto-change_hole_sheet_to_2:
feedback: {}
goal: {}
goal_default:
hole: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
hole:
type: object
required:
- hole
type: object
result: {}
required:
- goal
title: change_hole_sheet_to_2参数
type: object
type: UniLabJsonCommandAsync
auto-fill_plate:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: fill_plate参数
type: object
type: UniLabJsonCommandAsync
auto-fun_wuliao_test:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: fun_wuliao_test参数
type: object
type: UniLabJsonCommand
auto-func_allpack_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: 4200
assembly_type: 7
elec_num: null
elec_use_num: null
elec_vol: 50
file_path: /Users/sml/work
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
default: 4200
type: integer
assembly_type:
default: 7
type: integer
elec_num:
type: string
elec_use_num:
type: string
elec_vol:
default: 50
type: integer
file_path:
default: /Users/sml/work
type: string
required:
- elec_num
- elec_use_num
type: object
result: {}
required:
- goal
title: func_allpack_cmd参数
type: object
type: UniLabJsonCommand
auto-func_get_csv_export_status:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_get_csv_export_status参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_auto:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_auto参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_init:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_init参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_start:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_start参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_stop:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_stop参数
type: object
type: UniLabJsonCommand
auto-func_pack_get_msg_cmd:
feedback: {}
goal: {}
goal_default:
file_path: D:\coin_cell_data
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
default: D:\coin_cell_data
type: string
required: []
type: object
result: {}
required:
- goal
title: func_pack_get_msg_cmd参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_bottle_num:
feedback: {}
goal: {}
goal_default:
bottle_num: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
bottle_num:
type: string
required:
- bottle_num
type: object
result: {}
required:
- goal
title: func_pack_send_bottle_num参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_finished_cmd:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_send_finished_cmd参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_msg_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: null
assembly_type: null
elec_use_num: null
elec_vol: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
type: string
assembly_type:
type: string
elec_use_num:
type: string
elec_vol:
type: string
required:
- elec_use_num
- elec_vol
- assembly_type
- assembly_pressure
type: object
result: {}
required:
- goal
title: func_pack_send_msg_cmd参数
type: object
type: UniLabJsonCommand
auto-func_read_data_and_output:
feedback: {}
goal: {}
goal_default:
file_path: /Users/sml/work
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
default: /Users/sml/work
type: string
required: []
type: object
result: {}
required:
- goal
title: func_read_data_and_output参数
type: object
type: UniLabJsonCommand
auto-func_stop_read_data:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_stop_read_data参数
type: object
type: UniLabJsonCommand
auto-modify_deck_name:
feedback: {}
goal: {}
goal_default:
resource_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
resource_name:
type: string
required:
- resource_name
type: object
result: {}
required:
- goal
title: modify_deck_name参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-qiming_coin_cell_code:
feedback: {}
goal: {}
goal_default:
battery_clean_ignore: false
battery_pressure: 4000
battery_pressure_mode: true
fujipian_juzhendianwei: 0
fujipian_panshu: null
gemo_juzhendianwei: 0
gemopanshu: 0
lvbodian: true
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
battery_clean_ignore:
default: false
type: boolean
battery_pressure:
default: 4000
type: integer
battery_pressure_mode:
default: true
type: boolean
fujipian_juzhendianwei:
default: 0
type: integer
fujipian_panshu:
type: integer
gemo_juzhendianwei:
default: 0
type: integer
gemopanshu:
default: 0
type: integer
lvbodian:
default: true
type: boolean
required:
- fujipian_panshu
type: object
result: {}
required:
- goal
title: qiming_coin_cell_code参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
status_types:
data_assembly_coin_cell_num: int
data_assembly_pressure: int
data_assembly_time: float
data_axis_x_pos: float
data_axis_y_pos: float
data_axis_z_pos: float
data_coin_cell_code: str
data_coin_num: int
data_electrolyte_code: str
data_electrolyte_volume: int
data_glove_box_o2_content: float
data_glove_box_pressure: float
data_glove_box_water_content: float
data_open_circuit_voltage: float
data_pole_weight: float
request_rec_msg_status: bool
request_send_msg_status: bool
sys_mode: str
sys_status: str
type: python
config_info: []
description: ''
handles: []
icon: koudian.webp
init_param_schema:
config:
properties:
address:
default: 172.16.28.102
type: string
config:
type: object
debug_mode:
default: false
type: boolean
deck:
type: string
port:
default: '502'
type: string
required: []
type: object
data:
properties:
data_assembly_coin_cell_num:
type: integer
data_assembly_pressure:
type: integer
data_assembly_time:
type: number
data_axis_x_pos:
type: number
data_axis_y_pos:
type: number
data_axis_z_pos:
type: number
data_coin_cell_code:
type: string
data_coin_num:
type: integer
data_electrolyte_code:
type: string
data_electrolyte_volume:
type: integer
data_glove_box_o2_content:
type: number
data_glove_box_pressure:
type: number
data_glove_box_water_content:
type: number
data_open_circuit_voltage:
type: number
data_pole_weight:
type: number
request_rec_msg_status:
type: boolean
request_send_msg_status:
type: boolean
sys_mode:
type: string
sys_status:
type: string
required:
- sys_status
- sys_mode
- request_rec_msg_status
- request_send_msg_status
- data_assembly_coin_cell_num
- data_assembly_time
- data_open_circuit_voltage
- data_axis_x_pos
- data_axis_y_pos
- data_axis_z_pos
- data_pole_weight
- data_assembly_pressure
- data_electrolyte_volume
- data_coin_num
- data_coin_cell_code
- data_electrolyte_code
- data_glove_box_pressure
- data_glove_box_o2_content
- data_glove_box_water_content
type: object
registry_type: device
version: 1.0.0

View File

@@ -654,6 +654,31 @@ liquid_handler:
title: iter_tips参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-set_group:
feedback: {}
goal: {}
@@ -6170,6 +6195,31 @@ liquid_handler.prcxi:
title: move_to参数
type: object
type: UniLabJsonCommandAsync
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-run_protocol:
feedback: {}
goal: {}

View File

@@ -1,86 +0,0 @@
opsky_ATR30007:
category:
- characterization_optic
- opsky_ATR30007
class:
action_value_mappings:
auto-run_once:
feedback: {}
goal: {}
goal_default:
integration_time: '5000'
laser_power: '200'
norm_max: '1.0'
normalize: 'true'
save_csv: 'true'
save_plot: 'true'
handles: {}
result: {}
schema:
description: 执行一次站控-扫码-拉曼流程的大函数入口,参数以字符串形式传入。
properties:
feedback: {}
goal:
properties:
integration_time:
default: '5000'
type: string
laser_power:
default: '200'
type: string
norm_max:
default: '1.0'
type: string
normalize:
default: 'true'
type: string
save_csv:
default: 'true'
type: string
save_plot:
default: 'true'
type: string
required: []
type: object
result: {}
required: []
title: run_once 参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.opsky_Raman.opsky_ATR30007:opsky_ATR30007
status_types: {}
type: python
config_info: []
description: OPSKY ATR30007 光纤拉曼模块,提供单一入口大函数以执行一次完整流程。
handles: []
icon: ''
init_param_schema:
config:
properties:
plc_ip:
default: 192.168.1.88
type: string
plc_port:
default: 502
type: integer
robot_ip:
default: 192.168.1.200
type: string
robot_port:
default: 502
type: integer
scan_csv_file:
default: scan_results.csv
type: string
required:
- plc_ip
- plc_port
- robot_ip
- robot_port
- scan_csv_file
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -1,479 +1 @@
reaction_station.bioyond:
category:
- work_station
- reaction_station_bioyond
class:
action_value_mappings:
drip_back:
feedback: {}
goal:
assign_material_name: assign_material_name
temperature: temperature
time: time
titration_type: titration_type
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
temperature: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result: {}
schema:
description: 滴回去
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称(不能为空)
type: string
temperature:
description: 温度设定(°C)
type: string
time:
description: 观察时间(分钟)
type: string
titration_type:
description: 是否滴定(1=否, 2=是)
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
type: string
volume:
description: 分液公式(μL)
type: string
required:
- volume
- assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object
result: {}
required:
- goal
title: drip_back参数
type: object
type: UniLabJsonCommand
liquid_feeding_beaker:
feedback: {}
goal:
assign_material_name: assign_material_name
temperature: temperature
time: time
titration_type: titration_type
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
temperature: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result: {}
schema:
description: 液体进料烧杯
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称
type: string
temperature:
description: 温度设定(°C)
type: string
time:
description: 观察时间(分钟)
type: string
titration_type:
description: 是否滴定(1=否, 2=是)
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
type: string
volume:
description: 分液公式(μL)
type: string
required:
- volume
- assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object
result: {}
required:
- goal
title: liquid_feeding_beaker参数
type: object
type: UniLabJsonCommand
liquid_feeding_solvents:
feedback: {}
goal:
assign_material_name: assign_material_name
solvents: solvents
temperature: temperature
time: time
titration_type: titration_type
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
solvents: ''
temperature: '25.00'
time: '360'
titration_type: '1'
torque_variation: '2'
volume: ''
handles:
input:
- data_key: solvents
data_source: handle
data_type: object
handler_key: solvents
io_type: source
label: Solvents Data From Calculation Node
result: {}
schema:
description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称
type: string
solvents:
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
type: string
temperature:
default: '25.00'
description: 温度设定(°C),默认25.00
type: string
time:
default: '360'
description: 观察时间(分钟),默认360
type: string
titration_type:
default: '1'
description: 是否滴定(1=否, 2=是),默认1
type: string
torque_variation:
default: '2'
description: 是否观察 (1=否, 2=是),默认2
type: string
volume:
description: 分液量(μL)。可直接提供,或通过solvents参数自动计算
type: string
required:
- assign_material_name
type: object
result: {}
required:
- goal
title: liquid_feeding_solvents参数
type: object
type: UniLabJsonCommand
liquid_feeding_titration:
feedback: {}
goal:
assign_material_name: assign_material_name
temperature: temperature
time: time
titration_type: titration_type
torque_variation: torque_variation
volume_formula: volume_formula
goal_default:
assign_material_name: ''
temperature: ''
time: ''
titration_type: ''
torque_variation: ''
volume_formula: ''
handles: {}
result: {}
schema:
description: 液体进料(滴定)
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称
type: string
temperature:
description: 温度设定(°C)
type: string
time:
description: 观察时间(分钟)
type: string
titration_type:
description: 是否滴定(1=否, 2=是)
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
type: string
volume_formula:
description: 分液公式(μL)
type: string
required:
- volume_formula
- assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object
result: {}
required:
- goal
title: liquid_feeding_titration参数
type: object
type: UniLabJsonCommand
liquid_feeding_vials_non_titration:
feedback: {}
goal:
assign_material_name: assign_material_name
temperature: temperature
time: time
titration_type: titration_type
torque_variation: torque_variation
volume_formula: volume_formula
goal_default:
assign_material_name: ''
temperature: ''
time: ''
titration_type: ''
torque_variation: ''
volume_formula: ''
handles: {}
result: {}
schema:
description: 液体进料小瓶(非滴定)
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称
type: string
temperature:
description: 温度设定(°C)
type: string
time:
description: 观察时间(分钟)
type: string
titration_type:
description: 是否滴定(1=否, 2=是)
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
type: string
volume_formula:
description: 分液公式(μL)
type: string
required:
- volume_formula
- assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object
result: {}
required:
- goal
title: liquid_feeding_vials_non_titration参数
type: object
type: UniLabJsonCommand
process_and_execute_workflow:
feedback: {}
goal:
task_name: task_name
workflow_name: workflow_name
goal_default:
task_name: ''
workflow_name: ''
handles: {}
result: {}
schema:
description: 处理并执行工作流
properties:
feedback: {}
goal:
properties:
task_name:
description: 任务名称
type: string
workflow_name:
description: 工作流名称
type: string
required:
- workflow_name
- task_name
type: object
result: {}
required:
- goal
title: process_and_execute_workflow参数
type: object
type: UniLabJsonCommand
reactor_taken_in:
feedback: {}
goal:
assign_material_name: assign_material_name
cutoff: cutoff
temperature: temperature
goal_default:
assign_material_name: ''
cutoff: ''
temperature: ''
handles: {}
result: {}
schema:
description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称
type: string
cutoff:
description: 粘度上限
type: string
temperature:
description: 温度设定(°C)
type: string
required:
- cutoff
- temperature
- assign_material_name
type: object
result: {}
required:
- goal
title: reactor_taken_in参数
type: object
type: UniLabJsonCommand
reactor_taken_out:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result:
properties:
code:
description: 操作结果代码(1表示成功,0表示失败)
type: integer
return_info:
description: 操作结果详细信息
type: string
type: object
required:
- goal
title: reactor_taken_out参数
type: object
type: UniLabJsonCommand
solid_feeding_vials:
feedback: {}
goal:
assign_material_name: assign_material_name
material_id: material_id
temperature: temperature
time: time
torque_variation: torque_variation
goal_default:
assign_material_name: ''
material_id: ''
temperature: ''
time: ''
torque_variation: ''
handles: {}
result: {}
schema:
description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA)
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称(用于获取试剂瓶位ID)
type: string
material_id:
description: 粉末类型ID1=盐21分钟2=面粉27分钟3=BTDA38分钟
type: string
temperature:
description: 温度设定(°C)
type: string
time:
description: 观察时间(分钟)
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
type: string
required:
- assign_material_name
- material_id
- time
- torque_variation
- temperature
type: object
result: {}
required:
- goal
title: solid_feeding_vials参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation
protocol_type: []
status_types:
all_workflows: dict
bioyond_status: dict
workstation_status: dict
type: python
config_info: []
description: Bioyond反应站
handles: []
icon: reaction_station.webp
init_param_schema:
config:
properties:
config:
type: object
deck:
type: object
required: []
type: object
data:
properties:
all_workflows:
type: object
bioyond_status:
type: object
workstation_status:
type: object
required:
- bioyond_status
- all_workflows
- workstation_status
type: object
version: 1.0.0
{}

View File

@@ -834,174 +834,3 @@ linear_motion.toyo_xyz.sim:
mesh: toyo_xyz
type: device
version: 1.0.0
motor.iCL42:
category:
- robot_linear_motion
class:
action_value_mappings:
auto-execute_run_motor:
feedback: {}
goal: {}
goal_default:
mode: null
position: null
velocity: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 步进电机执行运动函数。直接执行电机运动命令,包括位置设定、速度控制和路径规划。该函数处理底层的电机控制协议,消除警告信息,设置运动参数并启动电机运行。适用于需要直接控制电机运动的应用场景。
properties:
feedback: {}
goal:
properties:
mode:
type: string
position:
type: number
velocity:
type: integer
required:
- mode
- position
- velocity
type: object
result: {}
required:
- goal
title: execute_run_motor参数
type: object
type: UniLabJsonCommand
auto-init_device:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: iCL42电机设备初始化函数。建立与iCL42步进电机驱动器的串口通信连接配置通信参数包括波特率、数据位、校验位等。该函数是电机使用前的必要步骤确保驱动器处于可控状态并准备接收运动指令。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: init_device参数
type: object
type: UniLabJsonCommand
auto-run_motor:
feedback: {}
goal: {}
goal_default:
mode: null
position: null
velocity: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 步进电机运动控制函数。根据指定的运动模式、目标位置和速度参数控制电机运动。支持多种运动模式和精确的位置控制,自动处理运动轨迹规划和执行。该函数提供异步执行和状态反馈,确保运动的准确性和可靠性。
properties:
feedback: {}
goal:
properties:
mode:
type: string
position:
type: number
velocity:
type: integer
required:
- mode
- position
- velocity
type: object
result: {}
required:
- goal
title: run_motor参数
type: object
type: UniLabJsonCommand
execute_command_from_outer:
feedback: {}
goal:
command: command
goal_default:
command: ''
handles: {}
result:
success: success
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: SendCmd_Feedback
type: object
goal:
properties:
command:
type: string
required:
- command
title: SendCmd_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SendCmd_Result
type: object
required:
- goal
title: SendCmd
type: object
type: SendCmd
module: unilabos.devices.motor.iCL42:iCL42Driver
status_types:
is_executing_run: bool
motor_position: int
success: bool
type: python
config_info: []
description: iCL42步进电机驱动器用于实验室设备的精密线性运动控制。该设备通过串口通信控制iCL42型步进电机驱动器支持多种运动模式和精确的位置、速度控制。具备位置反馈、运行状态监控和故障检测功能。适用于自动进样器、样品传送、精密定位平台等需要准确线性运动控制的实验室自动化设备。
handles: []
icon: ''
init_param_schema:
config:
properties:
device_address:
default: 1
type: integer
device_com:
default: COM9
type: string
required: []
type: object
data:
properties:
is_executing_run:
type: boolean
motor_position:
type: integer
success:
type: boolean
required:
- motor_position
- is_executing_run
- success
type: object
version: 1.0.0

View File

@@ -45,6 +45,31 @@ virtual_centrifuge:
title: initialize参数
type: object
type: UniLabJsonCommandAsync
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
centrifuge:
feedback:
current_speed: current_speed
@@ -335,6 +360,31 @@ virtual_column:
title: initialize参数
type: object
type: UniLabJsonCommandAsync
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
run_column:
feedback:
current_status: current_status
@@ -732,6 +782,31 @@ virtual_filter:
title: initialize参数
type: object
type: UniLabJsonCommandAsync
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
filter:
feedback:
current_status: current_status
@@ -1358,6 +1433,31 @@ virtual_heatchill:
title: initialize参数
type: object
type: UniLabJsonCommandAsync
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
heat_chill:
feedback:
status: status
@@ -2358,6 +2458,31 @@ virtual_rotavap:
title: initialize参数
type: object
type: UniLabJsonCommandAsync
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
evaporate:
feedback:
current_device: current_device
@@ -2690,6 +2815,31 @@ virtual_separator:
title: initialize参数
type: object
type: UniLabJsonCommandAsync
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
separate:
feedback:
current_status: status
@@ -3600,6 +3750,31 @@ virtual_solenoid_valve:
title: is_closed参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-reset:
feedback: {}
goal: {}
@@ -4177,6 +4352,31 @@ virtual_solid_dispenser:
title: parse_mol_string参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser
status_types:
current_reagent: str
@@ -4278,6 +4478,31 @@ virtual_stirrer:
title: initialize参数
type: object
type: UniLabJsonCommandAsync
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
start_stir:
feedback:
status: status
@@ -4995,6 +5220,31 @@ virtual_transfer_pump:
title: is_full参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-pull_plunger:
feedback: {}
goal: {}

File diff suppressed because it is too large Load Diff

View File

@@ -1,557 +0,0 @@
xrd_d7mate:
category:
- xrd_d7mate
class:
action_value_mappings:
auto-close:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: 安全关闭与XRD D7-Mate设备的TCP连接释放网络资源。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: close参数
type: object
type: UniLabJsonCommand
auto-connect:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: 与XRD D7-Mate设备建立TCP连接配置超时参数。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: connect参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
get_current_acquire_data:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
- goal
title: EmptyIn
type: object
type: EmptyIn
get_sample_down:
feedback: {}
goal:
sample_station: 1
goal_default:
int_input: 0
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: IntSingleInput_Feedback
type: object
goal:
properties:
int_input:
maximum: 2147483647
minimum: -2147483648
type: integer
required:
- int_input
title: IntSingleInput_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: IntSingleInput_Result
type: object
required:
- goal
title: IntSingleInput
type: object
type: IntSingleInput
get_sample_request:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
- goal
title: EmptyIn
type: object
type: EmptyIn
get_sample_status:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
- goal
title: EmptyIn
type: object
type: EmptyIn
send_sample_down_ready:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
- goal
title: EmptyIn
type: object
type: EmptyIn
send_sample_ready:
feedback: {}
goal:
end_theta: 80.0
exp_time: 0.5
increment: 0.02
sample_id: ''
start_theta: 10.0
goal_default:
end_theta: 80.0
exp_time: 0.5
increment: 0.02
sample_id: Sample001
start_theta: 10.0
handles: {}
result: {}
schema:
description: 送样完成后,发送样品信息和采集参数
properties:
feedback:
properties: {}
required: []
title: SampleReadyInput_Feedback
type: object
goal:
properties:
end_theta:
description: 结束角度≥5.5°且必须大于start_theta
minimum: 5.5
type: number
exp_time:
description: 曝光时间0.1-5.0秒)
maximum: 5.0
minimum: 0.1
type: number
increment:
description: 角度增量≥0.005
minimum: 0.005
type: number
sample_id:
description: 样品标识符
type: string
start_theta:
description: 起始角度≥5°
minimum: 5.0
type: number
required:
- sample_id
- start_theta
- end_theta
- increment
- exp_time
title: SampleReadyInput_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SampleReadyInput_Result
type: object
required:
- goal
title: SampleReadyInput
type: object
type: UniLabJsonCommand
set_power_off:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
- goal
title: EmptyIn
type: object
type: EmptyIn
set_power_on:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
- goal
title: EmptyIn
type: object
type: EmptyIn
set_voltage_current:
feedback: {}
goal:
current: 30.0
voltage: 40.0
goal_default:
current: 30.0
voltage: 40.0
handles: {}
result: {}
schema:
description: 设置高压电源电压和电流
properties:
feedback:
properties: {}
required: []
title: VoltageCurrentInput_Feedback
type: object
goal:
properties:
current:
description: 电流值mA
type: number
voltage:
description: 电压值kV
type: number
required:
- voltage
- current
title: VoltageCurrentInput_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: VoltageCurrentInput_Result
type: object
required:
- goal
title: VoltageCurrentInput
type: object
type: UniLabJsonCommand
start:
feedback: {}
goal: {}
goal_default:
end_theta: 80.0
exp_time: 0.1
increment: 0.05
sample_id: 样品名称
start_theta: 10.0
string: ''
wait_minutes: 3.0
handles: {}
result: {}
schema:
description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。
properties:
feedback: {}
goal:
properties:
end_theta:
description: 结束角度≥5.5°且必须大于start_theta
minimum: 5.5
type: string
exp_time:
description: 曝光时间0.1-5.0秒)
maximum: 5.0
minimum: 0.1
type: string
increment:
description: 角度增量≥0.005
minimum: 0.005
type: string
sample_id:
description: 样品标识符
type: string
start_theta:
description: 起始角度≥5°
minimum: 5.0
type: string
string:
description: 字符串格式的参数输入,如果提供则优先解析使用
type: string
wait_minutes:
description: 允许上样后等待分钟数
minimum: 0.0
type: number
required:
- sample_id
- start_theta
- end_theta
- increment
- exp_time
title: StartWorkflow_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: StartWorkflow_Result
type: object
required:
- goal
title: StartWorkflow
type: object
type: UniLabJsonCommand
start_auto_mode:
feedback: {}
goal:
status: true
goal_default:
status: true
handles: {}
result: {}
schema:
description: 启动或停止自动模式
properties:
feedback:
properties: {}
required: []
title: BoolSingleInput_Feedback
type: object
goal:
properties:
status:
description: True-启动自动模式False-停止自动模式
type: boolean
required:
- status
title: BoolSingleInput_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: BoolSingleInput_Result
type: object
required:
- goal
title: BoolSingleInput
type: object
type: UniLabJsonCommand
module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient
status_types: {}
type: python
config_info: []
description: XRD D7-Mate X射线衍射分析设备通过TCP通信实现远程控制与状态监控支持自动模式控制、上样流程、数据获取、下样流程和高压电源控制等功能。
handles: []
icon: ''
init_param_schema:
config:
properties:
host:
default: 127.0.0.1
type: string
port:
default: 6001
type: string
timeout:
default: 10.0
type: string
required: []
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -0,0 +1,92 @@
YB_20ml_fenyeping:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping
type: pylabrobot
description: YB_20ml_fenyeping
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_5ml_fenyeping:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping
type: pylabrobot
description: YB_5ml_fenyeping
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_jia_yang_tou_da:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da
type: pylabrobot
description: YB_jia_yang_tou_da
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_pei_ye_da_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle
type: pylabrobot
description: YB_pei_ye_da_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_pei_ye_xiao_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
type: pylabrobot
description: YB_pei_ye_xiao_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_qiang_tou:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou
type: pylabrobot
description: YB_qiang_tou
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle
type: pylabrobot
description: YB_ye_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -0,0 +1,182 @@
YB_100ml_yeti:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti
type: pylabrobot
description: YB_100ml_yeti
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_20ml_fenyepingban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban
type: pylabrobot
description: YB_20ml_fenyepingban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_5ml_fenyepingban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban
type: pylabrobot
description: YB_5ml_fenyepingban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6StockCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier
type: pylabrobot
description: YB_6StockCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6VialCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier
type: pylabrobot
description: YB_6VialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_gao_nian_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle
type: pylabrobot
description: YB_gao_nian_ye_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_gaonianye:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye
type: pylabrobot
description: YB_gaonianye
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_jia_yang_tou_da_Carrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier
type: pylabrobot
description: YB_jia_yang_tou_da_Carrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_peiyepingdaban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban
type: pylabrobot
description: YB_peiyepingdaban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_peiyepingxiaoban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban
type: pylabrobot
description: YB_peiyepingxiaoban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_qiang_tou_he:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he
type: pylabrobot
description: YB_qiang_tou_he
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_shi_pei_qi_kuai:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai
type: pylabrobot
description: YB_shi_pei_qi_kuai
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye
type: pylabrobot
description: YB_ye_Bottle_Carrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye_100ml_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle
type: pylabrobot
description: YB_ye_100ml_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,48 +0,0 @@
BIOYOND_PolymerStation_1BottleCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_1BottleCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_1FlaskCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_1FlaskCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_6StockCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6StockCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6StockCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_6VialCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6VialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,72 +0,0 @@
BIOYOND_PolymerStation_Liquid_Vial:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Liquid_Vial
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Reagent_Bottle:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reagent_Bottle
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Solid_Stock:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Stock
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Solid_Vial:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Vial
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Solution_Beaker:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solution_Beaker
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_TipBox:
category:
- bottles
- tip_boxes
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_TipBox
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Reactor:
category:
- bottles
- reactors
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reactor
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0

View File

@@ -22,21 +22,27 @@ BIOYOND_PolymerReactionStation_Deck:
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_Deck11:
BIOYOND_YB_Deck:
category:
- deck
class:
module: unilabos.resources.bioyond.decks:YB_Deck
module: unilabos.resources.bioyond.decks:BIOYOND_YB_Deck
type: pylabrobot
description: BIOYOND PolymerReactionStation Deck
description: BIOYOND_YB_Deck
handles: []
icon: 配液站.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0
CoincellDeck:
category:
- deck
class:
module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck
type: pylabrobot
description: CoincellDeck
handles: []
icon: yihua.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

View File

@@ -0,0 +1,56 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.YB_bottles import (
YB_pei_ye_xiao_Bottle,
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
def YIHUA_Electrolyte_12VialCarrier(name: str) -> BottleCarrier:
"""12瓶载架 - 2x6布局"""
# 载架尺寸 (mm)
carrier_size_x = 120.0
carrier_size_y = 250.0
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 35.0
bottle_spacing_x = 35.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (6 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=2,
num_items_y=6,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="Electrolyte_12VialCarrier",
)
carrier.num_items_x = 2
carrier.num_items_y = 6
carrier.num_items_z = 1
for i in range(12):
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}")
return carrier

View File

@@ -0,0 +1,179 @@
from typing import Any, Dict, Optional, TypedDict
from pylabrobot.resources import Resource as ResourcePLR
from pylabrobot.resources import Container
electrode_colors = {
"PositiveCan": "#ff0000",
"PositiveElectrode": "#cc3333",
"NegativeCan": "#000000",
"NegativeElectrode": "#666666",
"SpringWasher": "#8b7355",
"FlatWasher": "a9a9a9",
"AluminumFoil": "#ffcccc",
"Battery": "#00ff00",
}
class ElectrodeSheetState(TypedDict):
mass: float # 质量 (g)
material_type: str # 材料类型(铜、铝、不锈钢、弹簧钢等)
color: str # 材料类型对应的颜色
class ElectrodeSheet(ResourcePLR):
"""极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料"""
def __init__(
self,
name: str = "极片",
size_x=10,
size_y=10,
size_z=10,
category: str = "electrode_sheet",
model: Optional[str] = None,
):
"""初始化极片
Args:
name: 极片名称
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: ElectrodeSheetState = ElectrodeSheetState(
diameter=14,
thickness=0.1,
mass=0.5,
material_type="copper",
info=None
)
# TODO: 这个还要不要给self._unilabos_state赋值的
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 PositiveCan(name: str) -> ElectrodeSheet:
"""创建正极壳"""
sheet = ElectrodeSheet(name=name, size_x=12, size_y=12, size_z=3.0, model="PositiveCan")
sheet.load_state({"material_type": "aluminum", "color": electrode_colors["PositiveCan"]})
return sheet
def PositiveElectrode(name: str) -> ElectrodeSheet:
"""创建正极片"""
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.1, model="PositiveElectrode")
sheet.load_state({"material_type": "positive_electrode", "color": electrode_colors["PositiveElectrode"]})
return sheet
def NegativeCan(name: str) -> ElectrodeSheet:
"""创建负极壳"""
sheet = ElectrodeSheet(name=name, size_x=12, size_y=12, size_z=2.0, model="NegativeCan")
sheet.load_state({"material_type": "steel", "color": electrode_colors["NegativeCan"]})
return sheet
def NegativeElectrode(name: str) -> ElectrodeSheet:
"""创建负极片"""
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.1, model="NegativeElectrode")
sheet.load_state({"material_type": "negative_electrode", "color": electrode_colors["NegativeElectrode"]})
return sheet
def SpringWasher(name: str) -> ElectrodeSheet:
"""创建弹片"""
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.5, model="SpringWasher")
sheet.load_state({"material_type": "spring_steel", "color": electrode_colors["SpringWasher"]})
return sheet
def FlatWasher(name: str) -> ElectrodeSheet:
"""创建垫片"""
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.2, model="FlatWasher")
sheet.load_state({"material_type": "steel", "color": electrode_colors["FlatWasher"]})
return sheet
def AluminumFoil(name: str) -> ElectrodeSheet:
"""创建铝箔"""
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.05, model="AluminumFoil")
sheet.load_state({"material_type": "aluminum", "color": electrode_colors["AluminumFoil"]})
return sheet
class BatteryState(TypedDict):
color: str # 材料类型对应的颜色
electrolyte_name: str
data_electrolyte_code: str
open_circuit_voltage: float
assembly_pressure: float
electrolyte_volume: float
info: Optional[str] # 附加信息
class Battery(Container):
"""电池类 - 包含组装好的电池"""
def __init__(
self,
name: str = "电池",
size_x=12,
size_y=12,
size_z=6,
category: str = "battery",
model: Optional[str] = None,
):
"""初始化电池
Args:
name: 电池名称
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: BatteryState = BatteryState(
color=electrode_colors["Battery"],
electrolyte_name="",
data_electrolyte_code="",
open_circuit_voltage=0.0,
assembly_pressure=0.0,
electrolyte_volume=0.0,
info=None
)
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

View File

@@ -0,0 +1,344 @@
from typing import Dict, List, Optional, OrderedDict, Union, Callable
import math
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources import Resource, ResourceStack, ItemizedResource
from pylabrobot.resources.carrier import create_homogeneous_resources
from unilabos.resources.battery.electrode_sheet import (
PositiveCan, PositiveElectrode,
NegativeCan, NegativeElectrode,
SpringWasher, FlatWasher,
AluminumFoil,
Battery
)
class Magazine(ResourceStack):
"""子弹夹洞位类"""
def __init__(
self,
name: str,
direction: str = 'z',
resources: Optional[List[Resource]] = None,
max_sheets: int = 100,
**kwargs
):
"""初始化子弹夹洞位
Args:
name: 洞位名称
direction: 堆叠方向
resources: 资源列表
max_sheets: 最大极片数量
"""
super().__init__(
name=name,
direction=direction,
resources=resources,
)
self.max_sheets = max_sheets
@property
def size_x(self) -> float:
return self.get_size_x()
@property
def size_y(self) -> float:
return self.get_size_y()
@property
def size_z(self) -> float:
return self.get_size_z()
def serialize(self) -> dict:
return {
**super().serialize(),
"size_x": self.size_x or 10.0,
"size_y": self.size_y or 10.0,
"size_z": self.size_z or 10.0,
"max_sheets": self.max_sheets,
}
class MagazineHolder(ItemizedResource):
"""子弹夹类 - 有多个洞位,每个洞位放多个极片"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
ordered_items: Optional[Dict[str, Magazine]] = None,
ordering: Optional[OrderedDict[str, str]] = None,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
max_sheets_per_hole: int = 100,
cross_section_type: str = "circle",
category: str = "magazine_holder",
model: Optional[str] = None,
):
"""初始化子弹夹
Args:
name: 子弹夹名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
max_sheets_per_hole: 每个洞位最大极片数量
category: 类别
model: 型号
"""
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,
)
# 保存洞位的直径和深度
self.hole_diameter = hole_diameter
self.hole_depth = hole_depth
self.max_sheets_per_hole = max_sheets_per_hole
self.cross_section_type = cross_section_type
def serialize(self) -> dict:
return {
**super().serialize(),
"hole_diameter": self.hole_diameter,
"hole_depth": self.hole_depth,
"max_sheets_per_hole": self.max_sheets_per_hole,
"cross_section_type": self.cross_section_type,
}
def magazine_factory(
name: str,
size_x: float,
size_y: float,
size_z: float,
locations: List[Coordinate],
klasses: Optional[List[Callable[[str], str]]] = None,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
max_sheets_per_hole: int = 100,
category: str = "magazine_holder",
model: Optional[str] = None,
) -> 'MagazineHolder':
"""工厂函数:创建子弹夹
Args:
name: 子弹夹名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
locations: 洞位坐标列表
klasses: 每个洞位中极片的类列表
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
max_sheets_per_hole: 每个洞位最大极片数量
category: 类别
model: 型号
"""
for loc in locations:
loc.x -= hole_diameter / 2
loc.y -= hole_diameter / 2
# 创建洞位
_sites = create_homogeneous_resources(
klass=Magazine,
locations=locations,
resource_size_x=hole_diameter,
resource_size_y=hole_diameter,
name_prefix=name,
max_sheets=max_sheets_per_hole,
)
# 生成编号键
keys = [f"A{i+1}" for i in range(len(locations))]
sites = dict(zip(keys, _sites.values()))
holder = MagazineHolder(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=sites,
hole_diameter=hole_diameter,
hole_depth=hole_depth,
max_sheets_per_hole=max_sheets_per_hole,
category=category,
model=model,
)
if klasses is not None:
for i, klass in enumerate(klasses):
hole_key = keys[i]
hole = holder.children[i]
for j in reversed(range(max_sheets_per_hole)):
item_name = f"{hole_key}_sheet{j+1}"
item = klass(name=item_name)
hole.assign_child_resource(item)
return holder
def MagazineHolder_6_Cathode(
name: str,
size_x: float = 80.0,
size_y: float = 80.0,
size_z: float = 40.0,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 20.0,
max_sheets_per_hole: int = 100,
) -> MagazineHolder:
"""创建6孔子弹夹 - 六边形排布"""
center_x = size_x / 2
center_y = size_y / 2
locations = []
# 周围6个孔按六边形排布
for i in range(6):
angle = i * 60 * math.pi / 180 # 每60度一个孔
x = center_x + hole_spacing * math.cos(angle)
y = center_y + hole_spacing * math.sin(angle)
locations.append(Coordinate(x, y, size_z - hole_depth))
return magazine_factory(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
locations=locations,
klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan],
hole_diameter=hole_diameter,
hole_depth=hole_depth,
max_sheets_per_hole=max_sheets_per_hole,
category="magazine_holder",
model="MagazineHolder_6_Cathode",
)
def MagazineHolder_6_Anode(
name: str,
size_x: float = 80.0,
size_y: float = 80.0,
size_z: float = 40.0,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 20.0,
max_sheets_per_hole: int = 100,
) -> MagazineHolder:
"""创建6孔子弹夹 - 六边形排布"""
center_x = size_x / 2
center_y = size_y / 2
locations = []
# 周围6个孔按六边形排布
for i in range(6):
angle = i * 60 * math.pi / 180 # 每60度一个孔
x = center_x + hole_spacing * math.cos(angle)
y = center_y + hole_spacing * math.sin(angle)
locations.append(Coordinate(x, y, size_z - hole_depth))
return magazine_factory(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
locations=locations,
klasses=[SpringWasher, NegativeCan, NegativeCan, SpringWasher, NegativeCan, NegativeCan],
hole_diameter=hole_diameter,
hole_depth=hole_depth,
max_sheets_per_hole=max_sheets_per_hole,
category="magazine_holder",
model="MagazineHolder_6_Anode",
)
def MagazineHolder_6_Battery(
name: str,
size_x: float = 80.0,
size_y: float = 80.0,
size_z: float = 40.0,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 20.0,
max_sheets_per_hole: int = 100,
) -> MagazineHolder:
"""创建6孔子弹夹 - 六边形排布"""
center_x = size_x / 2
center_y = size_y / 2
locations = []
# 周围6个孔按六边形排布
for i in range(6):
angle = i * 60 * math.pi / 180 # 每60度一个孔
x = center_x + hole_spacing * math.cos(angle)
y = center_y + hole_spacing * math.sin(angle)
locations.append(Coordinate(x, y, size_z - hole_depth))
return magazine_factory(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
locations=locations,
klasses=None, # 初始化时,不放入装好的电池
hole_diameter=hole_diameter,
hole_depth=hole_depth,
max_sheets_per_hole=max_sheets_per_hole,
category="magazine_holder",
model="MagazineHolder_6_Battery",
)
def MagazineHolder_4_Cathode(
name: str,
) -> MagazineHolder:
"""创建4孔子弹夹 - 正方形四角排布"""
size_x: float = 80.0
size_y: float = 80.0
size_z: float = 10.0
hole_diameter: float = 14.0
hole_depth: float = 10.0
hole_spacing: float = 25.0
max_sheets_per_hole: int = 100
# 计算4个洞位的坐标正方形四角排布
center_x = size_x / 2
center_y = size_y / 2
offset = hole_spacing / 2
locations = [
Coordinate(center_x - offset, center_y - offset, size_z - hole_depth), # 左下
Coordinate(center_x + offset, center_y - offset, size_z - hole_depth), # 右下
Coordinate(center_x - offset, center_y + offset, size_z - hole_depth), # 左上
Coordinate(center_x + offset, center_y + offset, size_z - hole_depth), # 右上
]
return magazine_factory(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
locations=locations,
klasses=[AluminumFoil, PositiveElectrode, PositiveElectrode, PositiveElectrode],
hole_diameter=hole_diameter,
hole_depth=hole_depth,
max_sheets_per_hole=max_sheets_per_hole,
category="magazine_holder",
model="MagazineHolder_4_Cathode",
)

View File

@@ -0,0 +1,653 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.YB_bottles import (
YB_jia_yang_tou_da,
YB_ye_Bottle,
YB_ye_100ml_Bottle,
YB_gao_nian_ye_Bottle,
YB_5ml_fenyeping,
YB_20ml_fenyeping,
YB_pei_ye_xiao_Bottle,
YB_pei_ye_da_Bottle,
YB_qiang_tou,
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 30.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="Electrolyte_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
# for i in range(6):
# carrier[i] = YB_Solid_Vial(f"{name}_vial_{i+1}")
return carrier
def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
"""1瓶载架 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 100.0
# 烧杯尺寸
beaker_diameter = 80.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="Electrolyte_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
# carrier[0] = YB_Solution_Beaker(f"{name}_beaker_1")
return carrier
def YB_6StockCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 20.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="6StockCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
# for i in range(6):
# carrier[i] = YB_Solid_Stock(f"{name}_vial_{ordering[i]}")
return carrier
def YB_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 30.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
# for i in range(3):
# carrier[i] = YB_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
# for i in range(3, 6):
# carrier[i] = YB_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
return carrier
# 1瓶载架 - 单个中央位置
def YB_ye(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_ye",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_ye_Bottle(f"{name}_flask_1")
return carrier
# 高粘液瓶载架 - 单个中央位置
def YB_gaonianye(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_gaonianye",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1")
return carrier
# 100ml液体瓶载架 - 单个中央位置
def YB_100ml_yeti(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_100ml_yeti",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1")
return carrier
# 5ml分液瓶板 - 4x2布局8个位置
def YB_5ml_fenyepingban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 15.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_5ml_fenyepingban",
)
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_5ml_fenyeping(f"{name}_vial_{ordering[i]}")
return carrier
# 20ml分液瓶板 - 4x2布局8个位置
def YB_20ml_fenyepingban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 70.0
# 瓶位尺寸
bottle_diameter = 20.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_20ml_fenyepingban",
)
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_20ml_fenyeping(f"{name}_vial_{ordering[i]}")
return carrier
# 配液瓶(小)板 - 4x2布局8个位置
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 65.0
# 瓶位尺寸
bottle_diameter = 35.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_peiyepingxiaoban",
)
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
# 配液瓶(大)板 - 2x2布局4个位置
def YB_peiyepingdaban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 95.0
# 瓶位尺寸
bottle_diameter = 55.0
bottle_spacing_x = 60.0 # X方向间距
bottle_spacing_y = 60.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=2,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_peiyepingdaban",
)
carrier.num_items_x = 2
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "B1", "B2"]
for i in range(4):
carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
# 加样头(大)板 - 1x1布局1个位置
def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 95.0
# 瓶位尺寸
bottle_diameter = 35.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (1 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (1 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=1,
num_items_y=1,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_jia_yang_tou_da_Carrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_jia_yang_tou_da(f"{name}_head_1")
return carrier
def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier:
"""适配器块 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 30.0
# 适配器尺寸
adapter_diameter = 80.0
# 计算中央位置
center_x = (carrier_size_x - adapter_diameter) / 2
center_y = (carrier_size_y - adapter_diameter) / 2
center_z = 0.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=adapter_diameter,
resource_size_y=adapter_diameter,
name_prefix=name,
),
model="YB_shi_pei_qi_kuai",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
# 适配器块本身不包含瓶子,只是一个支撑结构
return carrier
def YB_qiang_tou_he(name: str) -> BottleCarrier:
"""枪头盒 - 8x12布局96个位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 55.0
# 枪头尺寸
tip_diameter = 10.0
tip_spacing_x = 9.0 # X方向间距
tip_spacing_y = 9.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2
start_y = (carrier_size_y - (8 - 1) * tip_spacing_y - tip_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=12,
num_items_y=8,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=tip_spacing_x,
item_dy=tip_spacing_y,
size_x=tip_diameter,
size_y=tip_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_qiang_tou_he",
)
carrier.num_items_x = 12
carrier.num_items_y = 8
carrier.num_items_z = 1
# 创建96个枪头
for i in range(96):
row = chr(65 + i // 12) # A-H
col = (i % 12) + 1 # 1-12
carrier[i] = YB_qiang_tou(f"{name}_tip_{row}{col}")
return carrier

View File

@@ -0,0 +1,163 @@
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数
"""加样头(大)"""
def YB_jia_yang_tou_da(
name: str,
diameter: float = 20.0,
height: float = 100.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建粉末瓶"""
return Bottle(
name=name,
diameter=diameter,# 未知
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_jia_yang_tou_da",
)
"""液1x1"""
def YB_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
max_volume: float = 50000.0, # 50mL
barcode: str = None,
) -> Bottle:
"""创建液体瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_ye_Bottle",
)
"""100ml液体"""
def YB_ye_100ml_Bottle(
name: str,
diameter: float = 50.0,
height: float = 90.0,
max_volume: float = 100000.0, # 100mL
barcode: str = None,
) -> Bottle:
"""创建100ml液体瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_100ml_yeti",
)
"""高粘液"""
def YB_gao_nian_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
max_volume: float = 50000.0, # 50mL
barcode: str = None,
) -> Bottle:
"""创建高粘液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="High_Viscosity_Liquid",
)
"""5ml分液瓶"""
def YB_5ml_fenyeping(
name: str,
diameter: float = 20.0,
height: float = 50.0,
max_volume: float = 5000.0, # 5mL
barcode: str = None,
) -> Bottle:
"""创建5ml分液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_5ml_fenyeping",
)
"""20ml分液瓶"""
def YB_20ml_fenyeping(
name: str,
diameter: float = 30.0,
height: float = 65.0,
max_volume: float = 20000.0, # 20mL
barcode: str = None,
) -> Bottle:
"""创建20ml分液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_20ml_fenyeping",
)
"""配液瓶(小)"""
def YB_pei_ye_xiao_Bottle(
name: str,
diameter: float = 35.0,
height: float = 60.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建配液瓶(小)"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_pei_ye_xiao_Bottle",
)
"""配液瓶(大)"""
def YB_pei_ye_da_Bottle(
name: str,
diameter: float = 55.0,
height: float = 100.0,
max_volume: float = 150000.0, # 150mL
barcode: str = None,
) -> Bottle:
"""创建配液瓶(大)"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_pei_ye_da_Bottle",
)
"""枪头"""
def YB_qiang_tou(
name: str,
diameter: float = 10.0,
height: float = 50.0,
max_volume: float = 1000.0, # 1mL
barcode: str = None,
) -> Bottle:
"""创建枪头"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_qiang_tou",
)

View File

@@ -1,46 +1,26 @@
from unilabos.resources.warehouse import WareHouse, warehouse_factory
from unilabos.resources.warehouse import WareHouse, YB_warehouse_factory
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 4x4x1仓库 (左侧堆栈: A01D04)"""
return warehouse_factory(
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=4,
num_items_x=1,
num_items_y=4,
num_items_z=1,
num_items_z=4,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=147.0,
item_dy=106.0,
item_dz=130.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
col_offset=0, # 从01开始: A01, A02, A03, A04
)
def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
"""创建BioYond 4x4x1仓库 (右侧堆栈: A05D08)"""
return warehouse_factory(
name=name,
num_items_x=4,
num_items_y=4,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=147.0,
item_dy=106.0,
item_dz=130.0,
category="warehouse",
col_offset=4, # 从05开始: A05, A06, A07, A08
)
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
"""创建BioYond 4x1x2仓库"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
@@ -57,22 +37,39 @@ def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
# 定义benyond的堆栈
def bioyond_warehouse_1x2x2(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_x=2,
num_items_y=2,
num_items_z=2,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
category="YB_warehouse",
)
def bioyond_warehouse_2x2x1(name: str) -> WareHouse:
"""创建BioYond 2x2x1仓库自动堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=2,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="YB_warehouse",
)
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=10,
num_items_y=1,
@@ -87,7 +84,7 @@ def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
)
def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_y=3,
@@ -102,7 +99,7 @@ def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
)
def bioyond_warehouse_2x1x3(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=1,
@@ -118,7 +115,7 @@ def bioyond_warehouse_2x1x3(name: str) -> WareHouse:
def bioyond_warehouse_3x3x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=3,
num_items_y=3,
@@ -133,7 +130,7 @@ def bioyond_warehouse_3x3x1(name: str) -> WareHouse:
)
def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=5,
num_items_y=1,
@@ -148,7 +145,7 @@ def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
)
def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=3,
num_items_y=3,
@@ -164,7 +161,7 @@ def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
"""创建BioYond开关盖加液模块台面"""
return warehouse_factory(
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=5,
@@ -179,31 +176,13 @@ def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
removed_positions=None
)
def bioyond_warehouse_1x8x4(name: str) -> WareHouse:
"""创建BioYond 8x4x1反应站堆栈A01D08"""
return warehouse_factory(
def bioyond_warehouse_3x5x1(name: str) -> WareHouse:
"""创建BioYond 3x5x1仓库手动堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=8, # 8列01-08
num_items_y=4, # 4行A-D
num_items_z=1, # 1层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=147.0,
item_dy=106.0,
item_dz=130.0,
category="warehouse",
)
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
"""创建BioYond站内试剂存放堆栈A01A02, 1行×2列"""
return warehouse_factory(
name=name,
num_items_x=2, # 2列01-02
num_items_y=1, # 1行A
num_items_z=1, # 1层
num_items_x=3,
num_items_y=5,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
@@ -213,31 +192,13 @@ def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
category="warehouse",
)
def bioyond_warehouse_liquid_preparation(name: str) -> WareHouse:
"""创建BioYond移液站内10%分装液体准备仓库A01B04"""
return warehouse_factory(
def bioyond_warehouse_20x1x1(name: str) -> WareHouse:
"""创建BioYond 20x1x1仓库粉末加样头堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=4, # 4列01-04
num_items_y=2, # 2行A-B
num_items_z=1, # 1层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
"""创建BioYond站内Tip盒堆栈A01B03用于存放枪头盒"""
return warehouse_factory(
name=name,
num_items_x=3, # 3列01-03
num_items_y=2, # 2行A-B
num_items_z=1, # 1层
num_items_x=20,
num_items_y=1,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,

View File

@@ -1,276 +0,0 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.bottles import (
BIOYOND_PolymerStation_Solid_Stock,
BIOYOND_PolymerStation_Solid_Vial,
BIOYOND_PolymerStation_Liquid_Vial,
BIOYOND_PolymerStation_Solution_Beaker,
BIOYOND_PolymerStation_Reagent_Bottle
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 30.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="BIOYOND_Electrolyte_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
for i in range(6):
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{i+1}")
return carrier
def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
"""1瓶载架 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 100.0
# 烧杯尺寸
beaker_diameter = 80.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="BIOYOND_Electrolyte_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1")
return carrier
def BIOYOND_PolymerStation_6StockCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 20.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="BIOYOND_PolymerStation_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
for i in range(6):
carrier[i] = BIOYOND_PolymerStation_Solid_Stock(f"{name}_vial_{ordering[i]}")
return carrier
def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 30.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="BIOYOND_PolymerStation_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
for i in range(3):
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
for i in range(3, 6):
carrier[i] = BIOYOND_PolymerStation_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
return carrier
def BIOYOND_PolymerStation_1BottleCarrier(name: str) -> BottleCarrier:
"""1瓶载架 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="BIOYOND_PolymerStation_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_flask_1")
return carrier
def BIOYOND_PolymerStation_1FlaskCarrier(name: str) -> BottleCarrier:
"""1瓶载架 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 70.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="BIOYOND_PolymerStation_1FlaskCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_bottle_1")
return carrier

View File

@@ -1,178 +0,0 @@
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数
def BIOYOND_PolymerStation_Solid_Stock(
name: str,
diameter: float = 20.0,
height: float = 100.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建粉末瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Solid_Stock",
)
def BIOYOND_PolymerStation_Solid_Vial(
name: str,
diameter: float = 25.0,
height: float = 60.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建粉末瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Solid_Vial",
)
def BIOYOND_PolymerStation_Liquid_Vial(
name: str,
diameter: float = 25.0,
height: float = 60.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建滴定液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Liquid_Vial",
)
def BIOYOND_PolymerStation_Solution_Beaker(
name: str,
diameter: float = 60.0,
height: float = 70.0,
max_volume: float = 200000.0, # 200mL
barcode: str = None,
) -> Bottle:
"""创建溶液烧杯"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Solution_Beaker",
)
def BIOYOND_PolymerStation_Reagent_Bottle(
name: str,
diameter: float = 70.0,
height: float = 120.0,
max_volume: float = 500000.0, # 500mL
barcode: str = None,
) -> Bottle:
"""创建试剂瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Reagent_Bottle",
)
def BIOYOND_PolymerStation_Reactor(
name: str,
diameter: float = 30.0,
height: float = 80.0,
max_volume: float = 50000.0, # 50mL
barcode: str = None,
) -> Bottle:
"""创建反应器"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Reactor",
)
def BIOYOND_PolymerStation_TipBox(
name: str,
size_x: float = 127.76, # 枪头盒宽度
size_y: float = 85.48, # 枪头盒长度
size_z: float = 100.0, # 枪头盒高度
barcode: str = None,
):
"""创建4×6枪头盒 (24个枪头)
Args:
name: 枪头盒名称
size_x: 枪头盒宽度 (mm)
size_y: 枪头盒长度 (mm)
size_z: 枪头盒高度 (mm)
barcode: 条形码
Returns:
TipBoxCarrier: 包含24个枪头孔位的枪头盒
"""
from pylabrobot.resources import Container, Coordinate
# 创建枪头盒容器
tip_box = Container(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category="tip_rack",
model="BIOYOND_PolymerStation_TipBox_4x6",
)
# 设置自定义属性
tip_box.barcode = barcode
tip_box.tip_count = 24 # 4行×6列
tip_box.num_items_x = 6 # 6列
tip_box.num_items_y = 4 # 4行
# 创建24个枪头孔位 (4行×6列)
# 假设孔位间距为 9mm
tip_spacing_x = 9.0 # 列间距
tip_spacing_y = 9.0 # 行间距
start_x = 14.38 # 第一个孔位的x偏移
start_y = 11.24 # 第一个孔位的y偏移
for row in range(4): # A, B, C, D
for col in range(6): # 1-6
spot_name = f"{chr(65 + row)}{col + 1}" # A1, A2, ..., D6
x = start_x + col * tip_spacing_x
y = start_y + row * tip_spacing_y
# 创建枪头孔位容器
tip_spot = Container(
name=spot_name,
size_x=8.0, # 单个枪头孔位大小
size_y=8.0,
size_z=size_z - 10.0, # 略低于盒子高度
category="tip_spot",
)
# 添加到枪头盒
tip_box.assign_child_resource(
tip_spot,
location=Coordinate(x=x, y=y, z=0)
)
return tip_box

View File

@@ -1,27 +1,13 @@
from os import name
from pickle import TRUE
from pylabrobot.resources import Deck, Coordinate, Rotation
from unilabos.resources.bioyond.warehouses import (
bioyond_warehouse_1x4x4,
bioyond_warehouse_1x4x4_right, # 新增:右侧仓库 (A05D08)
bioyond_warehouse_1x4x2,
bioyond_warehouse_liquid_and_lid_handling,
bioyond_warehouse_1x2x2,
bioyond_warehouse_1x3x3,
bioyond_warehouse_10x1x1,
bioyond_warehouse_3x3x1,
bioyond_warehouse_3x3x1_2,
bioyond_warehouse_5x1x1,
bioyond_warehouse_1x8x4,
bioyond_warehouse_reagent_storage,
bioyond_warehouse_liquid_preparation,
bioyond_warehouse_tipbox_storage, # 新增Tip盒堆栈
)
from unilabos.resources.bioyond.YB_warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, bioyond_warehouse_1x3x3, bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, bioyond_warehouse_5x1x1, bioyond_warehouse_20x1x1, bioyond_warehouse_2x2x1, bioyond_warehouse_3x5x1
class BIOYOND_PolymerReactionStation_Deck(Deck):
def __init__(
self,
self,
name: str = "PolymerReactionStation_Deck",
size_x: float = 2700.0,
size_y: float = 1080.0,
@@ -35,32 +21,24 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
def setup(self) -> None:
# 添加仓库
# 说明: 堆栈1物理上分为左右两部分
# - 堆栈1左: A01D04 (4行×4列, 位于反应站左侧)
# - 堆栈1右: A05D08 (4行×4列, 位于反应站右侧)
self.warehouses = {
"堆栈1": bioyond_warehouse_1x4x4("堆栈1"), # 左侧堆栈: A01D04
"堆栈1右": bioyond_warehouse_1x4x4_right("堆栈1右"), # 右侧堆栈: A05D08
"站内试剂存放堆栈": bioyond_warehouse_reagent_storage("站内试剂存放堆栈"), # A01A02
"移液站内10%分装液体准备仓库": bioyond_warehouse_liquid_preparation("移液站内10%分装液体准备仓库"), # A01B04
"站内Tip盒堆栈": bioyond_warehouse_tipbox_storage("站内Tip盒堆栈"), # A01B03, 存放枪头盒
"堆栈1": bioyond_warehouse_1x4x4("堆栈1"),
"堆栈2": bioyond_warehouse_1x4x4("堆栈2"),
"站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"),
}
self.warehouse_locations = {
"堆栈1": Coordinate(0.0, 430.0, 0.0), # 左侧位置
"堆栈1右": Coordinate(2500.0, 430.0, 0.0), # 右侧位置
"站内试剂存放堆栈": Coordinate(1100.0, 475.0, 0.0),
"移液站内10%分装液体准备仓库": Coordinate(1500.0, 300.0, 0.0),
"站内Tip盒堆栈": Coordinate(1800.0, 300.0, 0.0), # TODO: 根据实际位置调整坐标
"堆栈1": Coordinate(0.0, 430.0, 0.0),
"堆栈2": Coordinate(2550.0, 430.0, 0.0),
"站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0),
}
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
class BIOYOND_PolymerPreparationStation_Deck(Deck):
def __init__(
self,
self,
name: str = "PolymerPreparationStation_Deck",
size_x: float = 2700.0,
size_y: float = 1080.0,
@@ -92,7 +70,7 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
class BIOYOND_YB_Deck(Deck):
def __init__(
self,
self,
name: str = "YB_Deck",
size_x: float = 4150,
size_y: float = 1400.0,
@@ -107,40 +85,32 @@ class BIOYOND_YB_Deck(Deck):
def setup(self) -> None:
# 添加仓库
self.warehouses = {
"321窗口": bioyond_warehouse_1x2x2("321窗口"),
"43窗口": bioyond_warehouse_1x2x2("43窗口"),
"手动传递窗": bioyond_warehouse_1x3x3("手动传递窗"),
"手动传递窗": bioyond_warehouse_1x3x3("手动传递窗"),
"加样头堆栈": bioyond_warehouse_10x1x1("加样头堆栈"),
"加样头堆栈右": bioyond_warehouse_10x1x1("加样头堆栈右"),
"15ml配液堆栈左": bioyond_warehouse_3x3x1("15ml配液堆栈左"),
"母液加样右": bioyond_warehouse_3x3x1_2("母液加样右"),
"大瓶母液堆栈左": bioyond_warehouse_5x1x1("大瓶母液堆栈左"),
"大瓶母液堆栈右": bioyond_warehouse_5x1x1("大瓶母液堆栈右"),
"自动堆栈-左": bioyond_warehouse_2x2x1("自动堆栈-左"),
"自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"),
"手动堆栈-": bioyond_warehouse_3x5x1("手动堆栈-"),
"手动堆栈-": bioyond_warehouse_3x5x1("手动堆栈-"),
"粉末加样头堆栈": bioyond_warehouse_20x1x1("粉末加样头堆栈"),
"配液站内试剂仓库": bioyond_warehouse_3x3x1("配液站内试剂仓库"),
"试剂替换仓库": bioyond_warehouse_10x1x1("试剂替换仓库"),
}
# warehouse 的位置
self.warehouse_locations = {
"321窗口": Coordinate(-150.0, 158.0, 0.0),
"43窗口": Coordinate(4160.0, 158.0, 0.0),
"手动传递窗": Coordinate(-150.0, 877.0, 0.0),
"手动传递窗": Coordinate(4160.0, 877.0, 0.0),
"加样头堆栈": Coordinate(385.0, 1300.0, 0.0),
"加样头堆栈右": Coordinate(2187.0, 1300.0, 0.0),
"15ml配液堆栈左": Coordinate(749.0, 355.0, 0.0),
"母液加样右": Coordinate(2152.0, 333.0, 0.0),
"大瓶母液堆栈左": Coordinate(1164.0, 676.0, 0.0),
"大瓶母液堆栈右": Coordinate(2717.0, 676.0, 0.0),
"自动堆栈-左": Coordinate(-100.3, 171.5, 0.0),
"自动堆栈-右": Coordinate(3960.1, 155.9, 0.0),
"手动堆栈-": Coordinate(-213.3, 804.4, 0.0),
"手动堆栈-": Coordinate(3960.1, 807.6, 0.0),
"粉末加样头堆栈": Coordinate(415.0, 1301.0, 0.0),
"配液站内试剂仓库": Coordinate(2162.0, 437.0, 0.0),
"试剂替换仓库": Coordinate(1173.0, 802.0, 0.0),
}
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
def YB_Deck(name: str) -> Deck:
by=BIOYOND_YB_Deck(name=name)
by.setup()
return by
# def YB_Deck(name: str) -> Deck:
# # by=BIOYOND_YB_Deck(name=name)
# # by.setup()
# return None

View File

@@ -580,8 +580,6 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
"trash": "trash",
"deck": "deck",
"tip_rack": "tip_rack",
"warehouse": "warehouse",
"container": "container",
}
if source in replace_info:
return replace_info[source]
@@ -627,6 +625,8 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
Returns:
pylabrobot 格式的物料列表
"""
print("1:bioyond_materials:",bioyond_materials)
# print("2:type_mapping:",type_mapping)
plr_materials = []
for material in bioyond_materials:
@@ -634,24 +634,11 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
type_mapping.get(material.get("typeName"), ("RegularContainer", ""))[0] if type_mapping else "RegularContainer"
)
plr_material_result = initialize_resource(
plr_material: ResourcePLR = initialize_resource(
{"name": material["name"], "class": className}, resource_type=ResourcePLR
)
# initialize_resource 可能返回列表或单个对象
if isinstance(plr_material_result, list):
if len(plr_material_result) == 0:
logger.warning(f"物料 {material['name']} 初始化失败,跳过")
continue
plr_material = plr_material_result[0]
else:
plr_material = plr_material_result
# 确保 plr_material 是 ResourcePLR 实例
if not isinstance(plr_material, ResourcePLR):
logger.warning(f"物料 {material['name']} 不是有效的 ResourcePLR 实例,类型: {type(plr_material)}")
continue
print("plr_material:",plr_material)
print("code:",material.get("code", ""))
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
plr_material.unilabos_uuid = str(uuid.uuid4())
@@ -676,66 +663,25 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
]
bottle.code = detail.get("code", "")
else:
# 只对有 capacity 属性的容器(液体容器)处理液体追踪
if hasattr(plr_material, 'capacity'):
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
bottle.tracker.liquids = [
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
]
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
bottle.tracker.liquids = [
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
]
plr_materials.append(plr_material)
if deck and hasattr(deck, "warehouses"):
for loc in material.get("locations", []):
wh_name = loc.get("whName")
# 特殊处理: Bioyond的"堆栈1"需要映射到"堆栈1左"或"堆栈1右"
# 根据列号(x)判断: 1-4映射到左侧, 5-8映射到右侧
if wh_name == "堆栈1":
x_val = loc.get("x", 1)
if 1 <= x_val <= 4:
wh_name = "堆栈1左"
elif 5 <= x_val <= 8:
wh_name = "堆栈1右"
else:
logger.warning(f"物料 {material['name']} 的列号 x={x_val} 超出范围无法映射到堆栈1左或堆栈1右")
continue
if hasattr(deck, "warehouses") and wh_name in deck.warehouses:
warehouse = deck.warehouses[wh_name]
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
# PyLabRobot warehouse是列优先存储: A01,B01,C01,D01, A02,B02,C02,D02, ...
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
# 如果是右侧堆栈,需要调整列号 (5→1, 6→2, 7→3, 8→4)
if wh_name == "堆栈1右":
y = y - 4 # 将5-8映射到1-4
# 特殊处理对于1行×N列的横向warehouse如站内试剂存放堆栈
# Bioyond的y坐标表示线性位置序号而不是列号
if warehouse.num_items_y == 1:
# 1行warehouse: 直接用y作为线性索引
idx = y - 1
logger.debug(f"1行warehouse {wh_name}: y={y} → idx={idx}")
else:
# 多行warehouse: 使用列优先索引 (与Bioyond坐标系统一致)
# warehouse keys顺序: A01,B01,C01,D01, A02,B02,C02,D02, ...
# 索引计算: idx = (col-1) * num_rows + (row-1) + (layer-1) * (rows * cols)
row_idx = x - 1 # x表示行: 转为0-based
col_idx = y - 1 # y表示列: 转为0-based
layer_idx = z - 1 # 转为0-based
idx = layer_idx * (warehouse.num_items_x * warehouse.num_items_y) + col_idx * warehouse.num_items_y + row_idx
logger.debug(f"多行warehouse {wh_name}: x={x}(行),y={y}(列) → row={row_idx},col={col_idx} → idx={idx}")
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
warehouse = deck.warehouses[loc["whName"]]
idx = (
(loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y
+ (loc.get("x", 0) - 1) * warehouse.num_items_x
+ (loc.get("z", 0) - 1)
)
if 0 <= idx < warehouse.capacity:
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
warehouse[idx] = plr_material
logger.debug(f"✅ 物料 {material['name']} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})")
else:
logger.warning(f"物料 {material['name']} 的索引 {idx} 超出仓库 {wh_name} 容量 {warehouse.capacity}")
return plr_materials
@@ -772,8 +718,8 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
bottle = resource[0] if resource.capacity > 0 else resource
material = {
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
"name": resource.name if hasattr(resource, "name") else "",
"unit": "", # 修复Bioyond API 要求 unit 字段不能为空
"name": resource.get("name", ""),
"unit": "",
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
"Parameters": "{}"
}
@@ -817,8 +763,6 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
elif type(resource_class_config) == str:
# Allow special resource class names to be used
if resource_class_config not in lab_registry.resource_type_registry:
logger.warning(f"❌ 类 {resource_class_config} 不在 registry 中,返回原始配置")
logger.debug(f" 可用的类: {list(lab_registry.resource_type_registry.keys())[:10]}...")
return [resource_config]
# If the resource class is a string, look up the class in the
# resource_type_registry and import it

View File

@@ -29,7 +29,7 @@ class Bottle(Well):
size_x: float = 0.0,
size_y: float = 0.0,
size_z: float = 0.0,
barcode: Optional[str] = "",
barcode: Optional[str] = None,
category: str = "container",
model: Optional[str] = None,
**kwargs,

View File

@@ -8,7 +8,7 @@ from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def warehouse_factory(
def YB_warehouse_factory(
name: str,
num_items_x: int = 1,
num_items_y: int = 4,
@@ -23,7 +23,6 @@ def warehouse_factory(
empty: bool = False,
category: str = "warehouse",
model: Optional[str] = None,
col_offset: int = 0, # 新增列起始偏移量用于生成A05-D08等命名
):
# 创建16个板架位 (4层 x 4位置)
locations = []
@@ -45,11 +44,10 @@ def warehouse_factory(
name_prefix=name,
)
len_x, len_y = (num_items_x, num_items_y) if num_items_z == 1 else (num_items_y, num_items_z) if num_items_x == 1 else (num_items_x, num_items_z)
# 应用列偏移量支持A05-D08等命名
# 使用列优先顺序生成keys (与Bioyond坐标系统一致): A01,B01,C01,D01, A02,B02,C02,D02, ...
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)]
sites = {i: site for i, site in zip(keys, _sites.values())}
keys = [f"{LETTERS[len_y-1-j]}{str(i+1).zfill(2)}" for j in range(len_y) for i in range(len_x)]
sites = {i: site for i, site in zip(keys, _sites.values())}
return WareHouse(
name=name,
size_x=dx + item_dx * num_items_x,

View File

@@ -6,11 +6,12 @@ from typing import Optional, Dict, Any, List
import rclpy
from unilabos_msgs.srv._serial_command import SerialCommand_Response
from unilabos.app.register import register_devices_and_resources
from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker, ResourceTreeSet
from unilabos.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher
from unilabos_msgs.srv import SerialCommand # type: ignore
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor
from rclpy.executors import MultiThreadedExecutor
from rclpy.node import Node
from rclpy.timer import Timer
@@ -108,66 +109,51 @@ def slave(
rclpy_init_args: List[str] = ["--log-level", "debug"],
) -> None:
"""从节点函数"""
# 1. 初始化 ROS2
if not rclpy.ok():
rclpy.init(args=rclpy_init_args)
executor = rclpy.__executor
if not executor:
executor = rclpy.__executor = MultiThreadedExecutor()
devices_instances = {}
for device_config in devices_config.root_nodes:
device_id = device_config.res_content.id
if device_config.res_content.type != "device":
d = initialize_device_from_dict(device_id, device_config.get_nested_dict())
devices_instances[device_id] = d
# 默认初始化
# if d is not None and isinstance(d, Node):
# executor.add_node(d)
# else:
# print(f"Warning: Device {device_id} could not be initialized or is not a valid Node")
n = Node(f"slaveMachine_{BasicConfig.machine_name}", parameter_overrides=[])
executor.add_node(n)
if visual != "disable":
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
resource_mesh_manager = ResourceMeshManager(
resources_mesh_config,
resources_config, # type: ignore FIXME
resource_tracker=DeviceNodeResourceTracker(),
device_id="resource_mesh_manager",
)
joint_republisher = JointRepublisher("joint_republisher", DeviceNodeResourceTracker())
executor.add_node(resource_mesh_manager)
executor.add_node(joint_republisher)
# 1.5 启动 executor 线程
thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread")
thread.start()
# 2. 创建 Slave Machine Node
n = Node(f"slaveMachine_{BasicConfig.machine_name}", parameter_overrides=[])
executor.add_node(n)
# 3. 向 Host 报送节点信息和物料,获取 UUID 映射
uuid_mapping = {}
if not BasicConfig.slave_no_host:
# 3.1 报送节点信息
sclient = n.create_client(SerialCommand, "/node_info_update")
sclient.wait_for_service()
registry_config = {}
devices_to_register, resources_to_register = register_devices_and_resources(lab_registry, True)
registry_config.update(devices_to_register)
registry_config.update(resources_to_register)
request = SerialCommand.Request()
request.command = json.dumps(
{
"machine_name": BasicConfig.machine_name,
"type": "slave",
"devices_config": devices_config.dump(),
"registry_config": lab_registry.obtain_registry_device_info(),
"registry_config": registry_config,
},
ensure_ascii=False,
cls=TypeEncoder,
)
response = sclient.call_async(request).result()
sclient.call_async(request).result()
logger.info(f"Slave node info updated.")
# 使用新的 c2s_update_resource_tree 服务
rclient = n.create_client(SerialCommand, "/c2s_update_resource_tree")
rclient.wait_for_service()
# 序列化 ResourceTreeSet 为 JSON
# 3.2 报送物料树,获取 UUID 映射
if resources_config:
rclient = n.create_client(SerialCommand, "/c2s_update_resource_tree")
rclient.wait_for_service()
request = SerialCommand.Request()
request.command = json.dumps(
{
@@ -180,35 +166,61 @@ def slave(
},
ensure_ascii=False,
)
tree_response: SerialCommand_Response = rclient.call_async(request).result()
tree_response: SerialCommand_Response = rclient.call(request)
uuid_mapping = json.loads(tree_response.response)
# 创建反向映射new_uuid -> old_uuid
reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
for node in resources_config.root_nodes:
if node.res_content.type == "device":
for sub_node in node.children:
# 只有二级子设备
if sub_node.res_content.type != "device":
device_tracker = devices_instances[node.res_content.id].resource_tracker
# sub_node.res_content.uuid 已经是新UUID需要用旧UUID去查找
old_uuid = reverse_uuid_mapping.get(sub_node.res_content.uuid)
if old_uuid:
# 找到旧UUID使用UUID查找
resource_instance = device_tracker.figure_resource({"uuid": old_uuid})
else:
# 未找到旧UUID使用name查找
resource_instance = device_tracker.figure_resource({"name": sub_node.res_content.name})
device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
logger.info(f"Slave resource tree added. UUID mapping: {len(uuid_mapping)} nodes")
# 3.3 使用 UUID 映射更新 resources_config 的 UUID参考 client.py 逻辑)
old_uuids = {node.res_content.uuid: node for node in resources_config.all_nodes}
for old_uuid, node in old_uuids.items():
if old_uuid in uuid_mapping:
new_uuid = uuid_mapping[old_uuid]
node.res_content.uuid = new_uuid
# 更新所有子节点的 parent_uuid
for child in node.children:
child.res_content.parent_uuid = new_uuid
else:
logger.error("Slave模式不允许新增非设备节点下的物料")
continue
if tree_response:
logger.info(f"Slave resource tree added. Response: {tree_response.response}")
else:
logger.warning("Slave resource tree add response is None")
logger.warning(f"资源UUID未更新: {old_uuid}")
else:
logger.info("No resources to add.")
# 4. 初始化所有设备实例(此时 resources_config 的 UUID 已更新)
devices_instances = {}
for device_config in devices_config.root_nodes:
device_id = device_config.res_content.id
if device_config.res_content.type == "device":
d = initialize_device_from_dict(device_id, device_config.get_nested_dict())
if d is not None:
devices_instances[device_id] = d
logger.info(f"Device {device_id} initialized.")
else:
logger.warning(f"Device {device_id} initialization failed.")
# 5. 如果启用可视化,创建可视化相关节点
if visual != "disable":
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
# 将 ResourceTreeSet 转换为 list 用于 visual 组件
resources_list = (
[node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
if resources_config
else []
)
resource_mesh_manager = ResourceMeshManager(
resources_mesh_config,
resources_list,
resource_tracker=DeviceNodeResourceTracker(),
device_id="resource_mesh_manager",
)
joint_republisher = JointRepublisher("joint_republisher", DeviceNodeResourceTracker())
lh_joint_pub = LiquidHandlerJointPublisher(
resources_config=resources_list, resource_tracker=DeviceNodeResourceTracker()
)
executor.add_node(resource_mesh_manager)
executor.add_node(joint_republisher)
executor.add_node(lh_joint_pub)
# 7. 保持运行
while True:
time.sleep(1)

View File

@@ -638,6 +638,145 @@ class BaseROS2DeviceNode(Node, Generic[T]):
- remove: 从资源树中移除资源
"""
from pylabrobot.resources.resource import Resource as ResourcePLR
def _handle_add(
plr_resources: List[ResourcePLR], tree_set: ResourceTreeSet, additional_add_params: Dict[str, Any]
) -> Dict[str, Any]:
"""
处理资源添加操作的内部函数
Args:
plr_resources: PLR资源列表
tree_set: 资源树集合
additional_add_params: 额外的添加参数
Returns:
操作结果字典
"""
for plr_resource, tree in zip(plr_resources, tree_set.trees):
self.resource_tracker.add_resource(plr_resource)
self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
func = getattr(self.driver_instance, "resource_tree_add", None)
if callable(func):
func(plr_resources)
return {"success": True, "action": "add"}
def _handle_remove(resources_uuid: List[str]) -> Dict[str, Any]:
"""
处理资源移除操作的内部函数
Args:
resources_uuid: 要移除的资源UUID列表
Returns:
操作结果字典,包含移除的资源列表
"""
found_resources: List[List[Union[ResourcePLR, dict]]] = self.resource_tracker.figure_resource(
[{"uuid": uid} for uid in resources_uuid], try_mode=True
)
found_plr_resources = []
other_plr_resources = []
for found_resource in found_resources:
for resource in found_resource:
if issubclass(resource.__class__, ResourcePLR):
found_plr_resources.append(resource)
else:
other_plr_resources.append(resource)
# 调用driver的remove回调
func = getattr(self.driver_instance, "resource_tree_remove", None)
if callable(func):
func(found_plr_resources)
# 从parent卸载并从tracker移除
for plr_resource in found_plr_resources:
if plr_resource.parent is not None:
plr_resource.parent.unassign_child_resource(plr_resource)
self.resource_tracker.remove_resource(plr_resource)
self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点")
for other_plr_resource in other_plr_resources:
self.resource_tracker.remove_resource(other_plr_resource)
self.lab_logger().info(f"移除物料 {other_plr_resource} 及其子节点")
return {
"success": True,
"action": "remove",
# "removed_plr": found_plr_resources,
# "removed_other": other_plr_resources,
}
def _handle_update(
plr_resources: List[ResourcePLR], tree_set: ResourceTreeSet, additional_add_params: Dict[str, Any]
) -> Dict[str, Any]:
"""
处理资源更新操作的内部函数
Args:
plr_resources: PLR资源列表包含新状态
tree_set: 资源树集合
additional_add_params: 额外的参数
Returns:
操作结果字典
"""
for plr_resource, tree in zip(plr_resources, tree_set.trees):
states = plr_resource.serialize_all_state()
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
)
# Update操作中包含改名需要先remove再add
if original_instance.name != plr_resource.name:
old_name = original_instance.name
new_name = plr_resource.name
self.lab_logger().info(f"物料改名操作:{old_name} -> {new_name}")
# 收集所有相关的uuid包括子节点
_handle_remove([original_instance.unilabos_uuid])
original_instance.name = new_name
_handle_add([original_instance], tree_set, additional_add_params)
self.lab_logger().info(f"物料改名完成:{old_name} -> {new_name}")
# 常规更新:不涉及改名
original_parent_resource = original_instance.parent
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
self.lab_logger().info(
f"物料{original_instance} 原始父节点{original_parent_resource_uuid} "
f"目标父节点{target_parent_resource_uuid} 更新"
)
# 更新extra
if getattr(plr_resource, "unilabos_extra", None) is not None:
original_instance.unilabos_extra = getattr(plr_resource, "unilabos_extra") # type: ignore # noqa: E501
# 如果父节点变化,需要重新挂载
if (
original_parent_resource_uuid != target_parent_resource_uuid
and original_parent_resource is not None
):
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
# 加载状态
original_instance.load_all_state(states)
child_count = len(original_instance.get_all_children())
self.lab_logger().info(
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] " f"及其子节点 {child_count}"
)
# 调用driver的update回调
func = getattr(self.driver_instance, "resource_tree_update", None)
if callable(func):
func(plr_resources)
return {"success": True, "action": "update"}
try:
data = json.loads(req.command)
results = []
@@ -656,7 +795,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
].call_async(
SerialCommand.Request(
command=json.dumps(
{"data": {"data": resources_uuid, "with_children": True if action == "add" else "update"}, "action": "get"}
{"data": {"data": resources_uuid, "with_children": True if action == "add" else False}, "action": "get"}
)
)
) # type: ignore
@@ -664,68 +803,20 @@ class BaseROS2DeviceNode(Node, Generic[T]):
tree_set = ResourceTreeSet.from_raw_list(raw_nodes)
try:
if action == "add":
# 添加资源到资源跟踪器
if tree_set is None:
raise ValueError("tree_set不能为None")
plr_resources = tree_set.to_plr_resources()
for plr_resource, tree in zip(plr_resources, tree_set.trees):
self.resource_tracker.add_resource(plr_resource)
self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
func = getattr(self.driver_instance, "resource_tree_add", None)
if callable(func):
func(plr_resources)
results.append({"success": True, "action": "add"})
result = _handle_add(plr_resources, tree_set, additional_add_params)
results.append(result)
elif action == "update":
# 更新资源
if tree_set is None:
raise ValueError("tree_set不能为None")
plr_resources = tree_set.to_plr_resources()
for plr_resource, tree in zip(plr_resources, tree_set.trees):
states = plr_resource.serialize_all_state()
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
)
original_parent_resource = original_instance.parent
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
self.lab_logger().info(
f"物料{original_instance} 原始父节点{original_parent_resource_uuid} 目标父节点{target_parent_resource_uuid} 更新"
)
# todo: 对extra进行update
if getattr(plr_resource, "unilabos_extra", None) is not None:
original_instance.unilabos_extra = getattr(plr_resource, "unilabos_extra")
if original_parent_resource_uuid != target_parent_resource_uuid and original_parent_resource is not None:
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
original_instance.load_all_state(states)
self.lab_logger().info(
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())}"
)
func = getattr(self.driver_instance, "resource_tree_update", None)
if callable(func):
func(plr_resources)
results.append({"success": True, "action": "update"})
result = _handle_update(plr_resources, tree_set, additional_add_params)
results.append(result)
elif action == "remove":
# 移除资源
found_resources: List[List[Union[ResourcePLR, dict]]] = self.resource_tracker.figure_resource(
[{"uuid": uid} for uid in resources_uuid], try_mode=True
)
found_plr_resources = []
other_plr_resources = []
for found_resource in found_resources:
for resource in found_resource:
if issubclass(resource.__class__, ResourcePLR):
found_plr_resources.append(resource)
else:
other_plr_resources.append(resource)
func = getattr(self.driver_instance, "resource_tree_remove", None)
if callable(func):
func(found_plr_resources)
for plr_resource in found_plr_resources:
if plr_resource.parent is not None:
plr_resource.parent.unassign_child_resource(plr_resource)
self.resource_tracker.remove_resource(plr_resource)
self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点")
for other_plr_resource in other_plr_resources:
self.resource_tracker.remove_resource(other_plr_resource)
self.lab_logger().info(f"移除物料 {other_plr_resource} 及其子节点")
results.append({"success": True, "action": "remove"})
result = _handle_remove(resources_uuid)
results.append(result)
except Exception as e:
error_msg = f"Error processing {action} operation: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
@@ -734,7 +825,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 返回处理结果
result_json = {"results": results, "total": len(data)}
res.response = json.dumps(result_json, ensure_ascii=False)
res.response = json.dumps(result_json, ensure_ascii=False, cls=TypeEncoder)
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
except json.JSONDecodeError as e:
@@ -1004,9 +1095,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 通过资源跟踪器获取本地实例
final_resources = queried_resources if is_sequence else queried_resources[0]
final_resources = self.resource_tracker.figure_resource({"name": final_resources.name}, try_mode=False) if not is_sequence else [
self.resource_tracker.figure_resource({"name": res.name}, try_mode=False) for res in queried_resources
]
final_resources = (
self.resource_tracker.figure_resource({"name": final_resources.name}, try_mode=False)
if not is_sequence
else [
self.resource_tracker.figure_resource({"name": res.name}, try_mode=False)
for res in queried_resources
]
)
action_kwargs[k] = final_resources
except Exception as e:
@@ -1227,6 +1323,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
raise JsonCommandInitError(
f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}"
)
def _convert_resource_sync(self, resource_data: Dict[str, Any]):
"""同步转换资源数据为实例"""
# 创建资源查询请求

View File

@@ -165,29 +165,16 @@ class HostNode(BaseROS2DeviceNode):
# resources_config 的 root node 是
# # 创建反向映射new_uuid -> old_uuid
# reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
# for tree in resources_config.trees:
# node = tree.root_node
# if node.res_content.type == "device":
# if node.res_content.id == "host_node":
# continue
# # slave节点走c2s更新接口拿到add自行update uuid
# device_tracker = self.devices_instances[node.res_content.id].resource_tracker
# old_uuid = reverse_uuid_mapping.get(node.res_content.uuid)
# if old_uuid:
# # 找到旧UUID使用UUID查找
# resource_instance = device_tracker.uuid_to_resources.get(old_uuid)
# else:
# # 未找到旧UUID使用name查找
# resource_instance = device_tracker.figure_resource(
# {"name": node.res_content.name}
# )
# device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
# else:
# try:
# for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
# self.resource_tracker.add_resource(plr_resource)
# except Exception as ex:
# self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!")
for tree in resources_config.trees:
node = tree.root_node
if node.res_content.type == "device":
continue
else:
try:
for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
self._resource_tracker.add_resource(plr_resource)
except Exception as ex:
self.lab_logger().warning(f"[Host Node-Resource] 根节点物料{tree}序列化失败!")
except Exception as ex:
logger.error(f"[Host Node-Resource] 添加物料出错!\n{traceback.format_exc()}")
# 初始化Node基类传递空参数覆盖列表
@@ -665,7 +652,7 @@ class HostNode(BaseROS2DeviceNode):
if bCreate:
self.lab_logger().trace(f"Status created: {device_id}.{property_name} = {msg.data}")
else:
self.lab_logger().debug(f"Status updated: {device_id}.{property_name} = {msg.data}")
self.lab_logger().trace(f"Status updated: {device_id}.{property_name} = {msg.data}")
def send_goal(
self,
@@ -878,11 +865,10 @@ class HostNode(BaseROS2DeviceNode):
success = False
uuid_mapping = {}
if len(self.bridges) > 0:
from unilabos.app.web.client import HTTPClient
from unilabos.app.web.client import HTTPClient, http_client
client: HTTPClient = self.bridges[-1]
resource_start_time = time.time()
uuid_mapping = client.resource_tree_add(resource_tree_set, mount_uuid, first_add)
uuid_mapping = http_client.resource_tree_add(resource_tree_set, mount_uuid, first_add)
success = True
resource_end_time = time.time()
self.lab_logger().info(
@@ -990,9 +976,10 @@ class HostNode(BaseROS2DeviceNode):
"""
更新节点信息回调
"""
self.lab_logger().info(f"[Host Node] Node info update request received: {request}")
# self.lab_logger().info(f"[Host Node] Node info update request received: {request}")
try:
from unilabos.app.communication import get_communication_client
from unilabos.app.web.client import HTTPClient, http_client
info = json.loads(request.command)
if "SYNC_SLAVE_NODE_INFO" in info:
@@ -1001,10 +988,10 @@ class HostNode(BaseROS2DeviceNode):
edge_device_id = info["edge_device_id"]
self.device_machine_names[edge_device_id] = machine_name
else:
comm_client = get_communication_client()
registry_config = info["registry_config"]
for device_config in registry_config:
comm_client.publish_registry(device_config["id"], device_config)
devices_config = info.pop("devices_config")
registry_config = info.pop("registry_config")
if registry_config:
http_client.resource_registry({"resources": registry_config})
self.lab_logger().debug(f"[Host Node] Node info update: {info}")
response.response = "OK"
except Exception as e:
@@ -1030,10 +1017,9 @@ class HostNode(BaseROS2DeviceNode):
success = False
if len(self.bridges) > 0: # 边的提交待定
from unilabos.app.web.client import HTTPClient
from unilabos.app.web.client import HTTPClient, http_client
client: HTTPClient = self.bridges[-1]
r = client.resource_add(add_schema(resources))
r = http_client.resource_add(add_schema(resources))
success = bool(r)
response.success = success

View File

@@ -2,7 +2,7 @@ import traceback
import uuid
from pydantic import BaseModel, field_serializer, field_validator
from pydantic import Field
from typing import List, Tuple, Any, Dict, Literal, Optional, cast, TYPE_CHECKING
from typing import List, Tuple, Any, Dict, Literal, Optional, cast, TYPE_CHECKING, Union
from unilabos.utils.log import logger
@@ -848,15 +848,9 @@ class DeviceNodeResourceTracker(object):
extra: extra字典值
"""
if isinstance(resource, dict):
# ⭐ 修复合并extra而不是覆盖
current_extra = resource.get("extra", {})
current_extra.update(extra)
resource["extra"] = current_extra
resource["extra"] = extra
else:
# ⭐ 修复合并unilabos_extra而不是覆盖
current_extra = getattr(resource, "unilabos_extra", {})
current_extra.update(extra)
setattr(resource, "unilabos_extra", current_extra)
setattr(resource, "unilabos_extra", extra)
def _traverse_and_process(self, resource, process_func) -> int:
"""
@@ -900,7 +894,7 @@ class DeviceNodeResourceTracker(object):
new_uuid = name_to_uuid_map[resource_name]
self.set_resource_uuid(res, new_uuid)
self.uuid_to_resources[new_uuid] = res
logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}")
logger.trace(f"设置资源UUID: {resource_name} -> {new_uuid}")
return 1
return 0
@@ -923,7 +917,8 @@ class DeviceNodeResourceTracker(object):
if resource_name and resource_name in name_to_extra_map:
extra = name_to_extra_map[resource_name]
self.set_resource_extra(res, extra)
logger.debug(f"设置资源Extra: {resource_name} -> {extra}")
if len(extra):
logger.debug(f"设置资源Extra: {resource_name} -> {extra}")
return 1
return 0
@@ -933,7 +928,7 @@ class DeviceNodeResourceTracker(object):
"""
递归遍历资源树更新所有节点的uuid
Args:0
Args:
resource: 资源对象可以是dict或实例
uuid_map: uuid映射字典{old_uuid: new_uuid}
@@ -958,6 +953,27 @@ class DeviceNodeResourceTracker(object):
return self._traverse_and_process(resource, process)
def loop_gather_uuid(self, resource) -> List[str]:
"""
递归遍历资源树收集所有节点的uuid
Args:
resource: 资源对象可以是dict或实例
Returns:
收集到的uuid列表
"""
uuid_list = []
def process(res):
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
if current_uuid:
uuid_list.append(current_uuid)
return 0
self._traverse_and_process(resource, process)
return uuid_list
def _collect_uuid_mapping(self, resource):
"""
递归收集资源的 uuid 映射到 uuid_to_resources
@@ -971,14 +987,15 @@ class DeviceNodeResourceTracker(object):
if current_uuid:
old = self.uuid_to_resources.get(current_uuid)
self.uuid_to_resources[current_uuid] = res
logger.debug(
logger.trace(
f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}"
)
return 1
return 0
self._traverse_and_process(resource, process)
def _remove_uuid_mapping(self, resource):
def _remove_uuid_mapping(self, resource) -> int:
"""
递归清除资源的 uuid 映射
@@ -990,10 +1007,11 @@ class DeviceNodeResourceTracker(object):
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
if current_uuid and current_uuid in self.uuid_to_resources:
self.uuid_to_resources.pop(current_uuid)
logger.debug(f"移除资源UUID映射: {current_uuid} -> {res}")
logger.trace(f"移除资源UUID映射: {current_uuid} -> {res}")
return 1
return 0
self._traverse_and_process(resource, process)
return self._traverse_and_process(resource, process)
def parent_resource(self, resource):
if id(resource) in self.resource2parent_resource:
@@ -1048,13 +1066,12 @@ class DeviceNodeResourceTracker(object):
removed = True
break
if not removed:
# 递归清除uuid映射
count = self._remove_uuid_mapping(resource)
if not count:
logger.warning(f"尝试移除不存在的资源: {resource}")
return False
# 递归清除uuid映射
self._remove_uuid_mapping(resource)
# 清除 resource2parent_resource 中与该资源相关的映射
# 需要清除1) 该资源作为 key 的映射 2) 该资源作为 value 的映射
keys_to_remove = []
@@ -1077,7 +1094,9 @@ class DeviceNodeResourceTracker(object):
self.uuid_to_resources.clear()
self.resource2parent_resource.clear()
def figure_resource(self, query_resource, try_mode=False):
def figure_resource(
self, query_resource: Union[List[Union[dict, "PLRResource"]], dict, "PLRResource"], try_mode=False
) -> Union[List[Union[dict, "PLRResource", List[Union[dict, "PLRResource"]]]], dict, "PLRResource"]:
if isinstance(query_resource, list):
return [self.figure_resource(r, try_mode) for r in query_resource]
elif (

View File

@@ -191,6 +191,18 @@ def configure_logger(loglevel=None):
# 添加处理器到根日志记录器
root_logger.addHandler(console_handler)
# 降低第三方库的日志级别,避免过多输出
# pymodbus 库的日志太详细,设置为 WARNING
logging.getLogger('pymodbus').setLevel(logging.WARNING)
logging.getLogger('pymodbus.logging').setLevel(logging.WARNING)
logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING)
logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING)
# websockets 库的日志输出较多,设置为 WARNING
logging.getLogger('websockets').setLevel(logging.WARNING)
logging.getLogger('websockets.client').setLevel(logging.WARNING)
logging.getLogger('websockets.server').setLevel(logging.WARNING)
# 配置日志系统