57 Commits

Author SHA1 Message Date
Calvin Cao
6413828c59 Merge pull request #183 from sun7151887/yb_fix5
添加新威电池测试系统设备节点到配置文件
2025-12-02 17:06:07 +08:00
dijkstra402
5072f00836 添加新威电池测试系统设备节点到配置文件
- 在 new_cellconfig3c.json 中新增 NewareTester 设备
- 配置 IP:127.0.0.1, Port:502, Machine_ID:1
- 修复之前的 JSON 格式错误(重复对象和数组语法错误)
- 设备位置设置为 (1500, 0, 0),避免与其他设备重叠
- 包含功能说明: 720通道监控、2盘电池物料管理、CSV批量提交
2025-12-02 17:01:02 +08:00
Calvin Cao
9dfbe3246e Merge pull request #182 from sun7151887/yb_fix5
解决前端物料显示问题
2025-12-02 16:07:55 +08:00
dijkstra402
bef69db3b6 解决前端物料显示问题 2025-12-02 15:42:07 +08:00
Calvin Cao
a061bc2942 Merge pull request #181 from sun7151887/yb_fix5
修复遇到的参数错误和物料转换问题
2025-12-02 11:26:54 +08:00
dijkstra402
8c9e11c04f chore: 更新 Excel 模板文件
- 更新 2025092701.xlsx 配方文件
- 更新 material_template.xlsx 物料模板
2025-12-02 11:08:48 +08:00
dijkstra402
e4e3ec805a feat: 添加三阶段工作流函数和别名映射
- 在 BioyondCellWorkstation 添加 run_feeding_stage, run_liquid_preparation_stage, run_transfer_stage 三个阶段函数
- 在 host_node.py 添加 JSON_COMMAND_ALIASES 映射表,支持 run_feeding_stage -> auto_feeding4to3 别名
- 修复 create_orders 中 transfer_resource_to_another 参数名错误
- 简化 run_transfer_stage,注释掉物料转换逻辑,只保留核心转运功能
2025-12-02 11:05:36 +08:00
dijkstra402
d634316bce feat: enhance BioyondCellWorkstation and CoinCellAssembly workflows
- Added support for transferring resources between workstations with detailed logging.
- Introduced new methods for material conversion and resource registration.
- Updated YAML configurations to reflect new parameters and structures for workflows.
- Enhanced error handling and logging for better debugging and operational clarity.
2025-11-27 10:46:40 +08:00
Calvin Cao
f5446c6480 Merge pull request #174 from sun7151887/yb_fix5
奔曜实现物料流
2025-11-25 18:39:56 +08:00
dijkstra402
a98d25c16d feat: expose workflow material outputs 2025-11-25 18:27:34 +08:00
Calvin Cao
80b9589973 Merge pull request #173 from sun7151887/yb_fix5
fix: 修复 BioyondCellWorkstation 和 CoinCellAssembly 工作流程
2025-11-25 18:26:26 +08:00
dijkstra402
4d4bbcbae8 fix: 修复 BioyondCellWorkstation 和 CoinCellAssembly 工作流程
- 修复 run 方法的函数参数语法错误(冒号改为等号)
- 将 BioyondCellWorkstation 的 run 函数移入类内部
- 添加 run_bioyond_cell_workflow 方法支持可选的 1to2 步骤
- 更新相关 YAML 配置文件
2025-11-25 15:39:07 +08:00
Calvin Cao
fa9b2a08f2 Merge pull request #171 from Andy6M/feat/merge-neware-battery-systems
feat: Merge Neware monitoring and submission systems into unified driver
2025-11-24 15:24:02 +08:00
Xie Qiming
929d50f954 feat: Merge Neware monitoring and submission systems into unified driver 2025-11-21 20:13:51 +08:00
calvincao
e60bf29a7f feat(workstation): 实现奔曜与扣电池装配工作流统一配置执行接口
- 新增 `run_bioyond_cell_workflow` 函数以支持通过配置驱动奔曜配液与转运流程
- 新增 `run_coin_cell_packaging_workflow` 函数以支持通过配置驱动扣电池装配流程
- 两个函数均接受字典配置参数,实现初始化、操作调用及日志记录等功能的灵活控制- 提供 keep_alive机制用于持续运行场景
- 更新主程序入口逻辑,使用新工作流函数替代原有手动调用方式
- 支持从配置中读取实验样本、调度器设置以及各项操作开关和日志选项- 添加对 Excel 订单创建路径的配置化支持- 引入路径对象处理文件输入,提升跨平台兼容性- 增强错误提示信息,确保必要字段如 create_orders 的 excel_path 存在
- 封装所有设备动作至标准化函数调用结构,便于维护和扩展
2025-11-19 09:51:24 +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
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
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
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
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
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
36 changed files with 3876 additions and 1649 deletions

View File

@@ -99,7 +99,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine_four", "type": "MagazineHolder_4",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -140,7 +140,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -235,7 +235,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -330,7 +330,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -425,7 +425,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -523,7 +523,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine_four", "type": "MagazineHolder_4",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -564,7 +564,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -659,7 +659,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -754,7 +754,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -849,7 +849,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -949,7 +949,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -992,7 +992,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1087,7 +1087,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1182,7 +1182,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1277,7 +1277,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1372,7 +1372,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1467,7 +1467,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1567,7 +1567,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -1610,7 +1610,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1705,7 +1705,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1800,7 +1800,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1895,7 +1895,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1990,7 +1990,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2085,7 +2085,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2185,7 +2185,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -2228,7 +2228,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2323,7 +2323,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2418,7 +2418,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2513,7 +2513,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2608,7 +2608,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2703,7 +2703,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2803,7 +2803,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -2846,7 +2846,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2941,7 +2941,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3036,7 +3036,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3131,7 +3131,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3226,7 +3226,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3321,7 +3321,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3421,7 +3421,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -3464,7 +3464,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3559,7 +3559,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3654,7 +3654,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3749,7 +3749,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3844,7 +3844,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3939,7 +3939,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4039,7 +4039,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -4082,7 +4082,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4177,7 +4177,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4272,7 +4272,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4367,7 +4367,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4462,7 +4462,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4557,7 +4557,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.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": []
}

View File

@@ -1,3 +1,4 @@
{ {
"nodes": [ "nodes": [
{ {
@@ -47,22 +48,90 @@
{ {
"id": "BatteryStation", "id": "BatteryStation",
"name": "扣电工作站", "name": "扣电工作站",
"parent": null,
"children": [ "children": [
"coin_cell_deck" "coin_cell_deck"
], ],
"parent": null,
"type": "device", "type": "device",
"class": "coincellassemblyworkstation_device", "class":"coincellassemblyworkstation_device",
"position": {
"x": 600,
"y": 400,
"z": 0
},
"config": { "config": {
"debug_mode": false, "deck": {
"data": {
"_resource_child_name": "YB_YH_Deck",
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck"
}
},
"protocol_type": [] "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": {}
},
{
"id": "NewareTester",
"name": "新威电池测试系统",
"parent": null,
"children": [],
"type": "device",
"class": "neware_battery_test_system",
"config": {
"ip": "127.0.0.1",
"port": 502,
"machine_id": 1,
"devtype": "27",
"timeout": 20,
"size_x": 500.0,
"size_y": 500.0,
"size_z": 2000.0
},
"position": {
"size": {
"height": 1600,
"width": 1200,
"depth": 800
},
"position": {
"x": 1500,
"y": 0,
"z": 0
}
},
"data": {
"功能说明": "新威电池测试系统提供720通道监控和CSV批量提交功能",
"监控功能": "支持720个通道的实时状态监控、2盘电池物料管理、状态导出等",
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务"
} }
} }
], ],
"links": [] "links": []
} }

View File

@@ -1,29 +0,0 @@
{
"nodes": [
{
"id": "NEWARE_BATTERY_TEST_SYSTEM",
"name": "Neware Battery Test System",
"parent": null,
"type": "device",
"class": "neware_battery_test_system",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"ip": "127.0.0.1",
"port": 502,
"machine_id": 1,
"devtype": "27",
"timeout": 20,
"size_x": 500.0,
"size_y": 500.0,
"size_z": 2000.0
},
"data": {},
"children": []
}
],
"links": []
}

View File

@@ -0,0 +1,8 @@
from .neware_battery_test_system import NewareBatteryTestSystem
from .neware_driver import build_start_command, start_test
__all__ = [
"NewareBatteryTestSystem",
"build_start_command",
"start_test",
]

View File

@@ -0,0 +1,3 @@
Timestamp,Battery_Count,Assembly_Time,Open_Circuit_Voltage,Pole_Weight,Assembly_Pressure,Battery_Code,Electrolyte_Code,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʺ<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>mah/g,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ,<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>ź<EFBFBD>,ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2025/10/29 17:32,7,5,0.11299999803304672,18.049999237060547,3593,Li000595,Si-Gr001,9.2,0.954,469,SiGr_Li,1,1,2
2025/10/30 17:49,2,5,0,13.109999895095825,4094,YS101224,NoRead88,5.2,0.92,190,SiGr_Li,2,1,1
1 Timestamp Battery_Count Assembly_Time Open_Circuit_Voltage Pole_Weight Assembly_Pressure Battery_Code Electrolyte_Code 集流体质量 活性物质含量 克容量mah/g 电池体系 设备号 排号 通道号
2 2025/10/29 17:32 7 5 0.11299999803304672 18.049999237060547 3593 Li000595 Si-Gr001 9.2 0.954 469 SiGr_Li 1 1 2
3 2025/10/30 17:49 2 5 0 13.109999895095825 4094 YS101224 NoRead88 5.2 0.92 190 SiGr_Li 2 1 1

View File

@@ -0,0 +1,33 @@
{
"nodes": [
{
"id": "NEWARE_BATTERY_TEST_SYSTEM",
"name": "Neware Battery Test System",
"parent": null,
"type": "device",
"class": "neware_battery_test_system",
"position": {
"x": 620.0,
"y": 200.0,
"z": 0
},
"config": {
"ip": "127.0.0.1",
"port": 502,
"machine_id": 1,
"devtype": "27",
"timeout": 20,
"size_x": 500.0,
"size_y": 500.0,
"size_z": 2000.0
},
"data": {
"功能说明": "新威电池测试系统提供720通道监控和CSV批量提交功能",
"监控功能": "支持720个通道的实时状态监控、2盘电池物料管理、状态导出等",
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务。CSV必须包含: Battery_Code, Pole_Weight, 集流体质量, 活性物质含量, 克容量mah/g, 电池体系, 设备号, 排号, 通道号"
},
"children": []
}
],
"links": []
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,8 @@
- 状态类型: working/stop/finish/protect/pause/false/unknown - 状态类型: working/stop/finish/protect/pause/false/unknown
""" """
import os
import sys
import socket import socket
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import json import json
@@ -21,7 +23,6 @@ from dataclasses import dataclass
from typing import Any, Dict, List, Optional, TypedDict from typing import Any, Dict, List, Optional, TypedDict
from pylabrobot.resources import ResourceHolder, Coordinate, create_ordered_items_2d, Deck, Plate from pylabrobot.resources import ResourceHolder, Coordinate, create_ordered_items_2d, Deck, Plate
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
@@ -56,13 +57,6 @@ class BatteryTestPositionState(TypedDict):
status: str # 通道状态 status: str # 通道状态
color: str # 状态对应颜色 color: str # 状态对应颜色
# 额外的inquire协议字段
relativetime: float # 相对时间 (s)
open_or_close: int # 0=关闭, 1=打开
step_type: str # 步骤类型
cycle_id: int # 循环ID
step_id: int # 步骤ID
log_code: str # 日志代码
class BatteryTestPosition(ResourceHolder): class BatteryTestPosition(ResourceHolder):
@@ -142,9 +136,9 @@ class NewareBatteryTestSystem:
devtype: str = None, devtype: str = None,
timeout: int = None, timeout: int = None,
size_x: float = 500.0, size_x: float = 50,
size_y: float = 500.0, size_y: float = 50,
size_z: float = 2000.0, size_z: float = 20,
): ):
""" """
初始化新威电池测试系统 初始化新威电池测试系统
@@ -162,6 +156,12 @@ class NewareBatteryTestSystem:
self.machine_id = machine_id self.machine_id = machine_id
self.devtype = devtype or self.DEVTYPE self.devtype = devtype or self.DEVTYPE
self.timeout = timeout or self.TIMEOUT self.timeout = timeout or self.TIMEOUT
# 存储设备物理尺寸
self.size_x = size_x
self.size_y = size_y
self.size_z = size_z
self._last_status_update = None self._last_status_update = None
self._cached_status = {} self._cached_status = {}
self._ros_node: Optional[ROS2WorkstationNode] = None # ROS节点引用由框架设置 self._ros_node: Optional[ROS2WorkstationNode] = None # ROS节点引用由框架设置
@@ -192,8 +192,9 @@ class NewareBatteryTestSystem:
def _setup_material_management(self): def _setup_material_management(self):
"""设置物料管理系统""" """设置物料管理系统"""
# 第1盘5行8列网格 (A1-E8) - 5行对应subdevid 1-58列对应chlid 1-8 # 第1盘5行8列网格 (A1-E8) - 5行对应subdevid 1-58列对应chlid 1-8
# 先给物料设置一个最大的Deck # 先给物料设置一个最大的Deck,并设置其在空间中的位置
deck_main = Deck("ADeckName", 200, 200, 200)
deck_main = Deck("ADeckName", 2000, 1800, 100, origin=Coordinate(2000,2000,0))
plate1_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d( plate1_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d(
BatteryTestPosition, BatteryTestPosition,
@@ -202,8 +203,8 @@ class NewareBatteryTestSystem:
dx=10, dx=10,
dy=10, dy=10,
dz=0, dz=0,
item_dx=45, item_dx=65,
item_dy=45 item_dy=65
) )
plate1 = Plate("P1", 400, 300, 50, ordered_items=plate1_resources) plate1 = Plate("P1", 400, 300, 50, ordered_items=plate1_resources)
deck_main.assign_child_resource(plate1, location=Coordinate(0, 0, 0)) deck_main.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
@@ -232,11 +233,15 @@ class NewareBatteryTestSystem:
num_items_y=5, # 5行对应subdevid 6-10即A-E num_items_y=5, # 5行对应subdevid 6-10即A-E
dx=10, dx=10,
dy=10, dy=10,
dz=100, # Z轴偏移100mm dz=0,
item_dx=65, item_dx=65,
item_dy=65 item_dy=65
) )
plate2 = Plate("P2", 400, 300, 50, ordered_items=plate2_resources)
deck_main.assign_child_resource(plate2, location=Coordinate(0, 350, 0))
# 为第2盘资源添加P2_前缀 # 为第2盘资源添加P2_前缀
self.station_resources_plate2 = {} self.station_resources_plate2 = {}
for name, resource in plate2_resources.items(): for name, resource in plate2_resources.items():
@@ -306,55 +311,132 @@ class NewareBatteryTestSystem:
def _update_plate_resources(self, subunits: Dict): def _update_plate_resources(self, subunits: Dict):
"""更新两盘电池资源的状态""" """更新两盘电池资源的状态"""
# 第1盘subdevid 1-5 映射到 P1_A1-P1_E8 (5行8列) # 第1盘subdevid 1-5 映射到 8列5行网格 (列0-7, 行0-4)
for subdev_id in range(1, 6): # subdevid 1-5 for subdev_id in range(1, 6): # subdevid 1-5
status_row = subunits.get(subdev_id, {}) status_row = subunits.get(subdev_id, {})
for chl_id in range(1, 9): # chlid 1-8 for chl_id in range(1, 9): # chlid 1-8
try: try:
# 计算在5×8网格中的位置 # 根据用户描述:第一个是(0,0),最后一个是(7,4)
row_idx = (subdev_id - 1) # 0-4 (对应A-E) # 说明是8列5行列从0开始行从0开始
col_idx = (chl_id - 1) # 0-7 (对应1-8) col_idx = (chl_id - 1) # 0-7 (chlid 1-8 -> 列0-7)
resource_name = f"P1_{self.LETTERS[row_idx]}{col_idx + 1}" row_idx = (subdev_id - 1) # 0-4 (subdevid 1-5 -> 行0-4)
# 尝试多种可能的资源命名格式
possible_names = [
f"P1_batterytestposition_{col_idx}_{row_idx}", # 用户提到的格式
f"P1_{self.LETTERS[row_idx]}{col_idx + 1}", # 原有的A1-E8格式
f"P1_{self.LETTERS[row_idx].lower()}{col_idx + 1}", # 小写字母格式
]
r = None
resource_name = None
for name in possible_names:
if name in self.station_resources:
r = self.station_resources[name]
resource_name = name
break
r = self.station_resources.get(resource_name)
if r: if r:
status_channel = status_row.get(chl_id, {}) status_channel = status_row.get(chl_id, {})
metrics = status_channel.get("metrics", {})
# 构建BatteryTestPosition状态数据移除capacity和energy
channel_state = { channel_state = {
# 基本测量数据
"voltage": metrics.get("voltage_V", 0.0),
"current": metrics.get("current_A", 0.0),
"time": metrics.get("totaltime_s", 0.0),
# 状态信息
"status": status_channel.get("state", "unknown"), "status": status_channel.get("state", "unknown"),
"color": status_channel.get("color", self.STATUS_COLOR["unknown"]), "color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
"voltage": status_channel.get("voltage_V", 0.0),
"current": status_channel.get("current_A", 0.0), # 通道名称标识
"time": status_channel.get("totaltime_s", 0.0), "Channel_Name": f"{self.machine_id}-{subdev_id}-{chl_id}",
} }
r.load_state(channel_state) r.load_state(channel_state)
except (KeyError, IndexError):
# 调试信息
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
self._ros_node.lab_logger().debug(
f"更新P1资源状态: {resource_name} <- subdev{subdev_id}/chl{chl_id} "
f"状态:{channel_state['status']}"
)
else:
# 如果找不到资源,记录调试信息
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
self._ros_node.lab_logger().debug(
f"P1未找到资源: subdev{subdev_id}/chl{chl_id} -> 尝试的名称: {possible_names}"
)
except (KeyError, IndexError) as e:
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
self._ros_node.lab_logger().debug(f"P1映射错误: subdev{subdev_id}/chl{chl_id} - {e}")
continue continue
# 第2盘subdevid 6-10 映射到 P2_A1-P2_E8 (5行8列) # 第2盘subdevid 6-10 映射到 8列5行网格 (列0-7, 行0-4)
for subdev_id in range(6, 11): # subdevid 6-10 for subdev_id in range(6, 11): # subdevid 6-10
status_row = subunits.get(subdev_id, {}) status_row = subunits.get(subdev_id, {})
for chl_id in range(1, 9): # chlid 1-8 for chl_id in range(1, 9): # chlid 1-8
try: try:
# 计算在5×8网格中的位置 col_idx = (chl_id - 1) # 0-7 (chlid 1-8 -> 列0-7)
row_idx = (subdev_id - 6) # 0-4 (subdevid 6->0, 7->1, ..., 10->4) (对应A-E) row_idx = (subdev_id - 6) # 0-4 (subdevid 6-10 -> 行0-4)
col_idx = (chl_id - 1) # 0-7 (对应1-8)
resource_name = f"P2_{self.LETTERS[row_idx]}{col_idx + 1}" # 尝试多种可能的资源命名格式
possible_names = [
f"P2_batterytestposition_{col_idx}_{row_idx}", # 用户提到的格式
f"P2_{self.LETTERS[row_idx]}{col_idx + 1}", # 原有的A1-E8格式
f"P2_{self.LETTERS[row_idx].lower()}{col_idx + 1}", # 小写字母格式
]
r = None
resource_name = None
for name in possible_names:
if name in self.station_resources:
r = self.station_resources[name]
resource_name = name
break
r = self.station_resources.get(resource_name)
if r: if r:
status_channel = status_row.get(chl_id, {}) status_channel = status_row.get(chl_id, {})
metrics = status_channel.get("metrics", {})
# 构建BatteryTestPosition状态数据移除capacity和energy
channel_state = { channel_state = {
# 基本测量数据
"voltage": metrics.get("voltage_V", 0.0),
"current": metrics.get("current_A", 0.0),
"time": metrics.get("totaltime_s", 0.0),
# 状态信息
"status": status_channel.get("state", "unknown"), "status": status_channel.get("state", "unknown"),
"color": status_channel.get("color", self.STATUS_COLOR["unknown"]), "color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
"voltage": status_channel.get("voltage_V", 0.0),
"current": status_channel.get("current_A", 0.0), # 通道名称标识
"time": status_channel.get("totaltime_s", 0.0), "Channel_Name": f"{self.machine_id}-{subdev_id}-{chl_id}",
} }
r.load_state(channel_state) r.load_state(channel_state)
except (KeyError, IndexError):
# 调试信息
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
self._ros_node.lab_logger().debug(
f"更新P2资源状态: {resource_name} <- subdev{subdev_id}/chl{chl_id} "
f"状态:{channel_state['status']}"
)
else:
# 如果找不到资源,记录调试信息
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
self._ros_node.lab_logger().debug(
f"P2未找到资源: subdev{subdev_id}/chl{chl_id} -> 尝试的名称: {possible_names}"
)
except (KeyError, IndexError) as e:
if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
self._ros_node.lab_logger().debug(f"P2映射错误: subdev{subdev_id}/chl{chl_id} - {e}")
continue continue
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": list(self.station_resources.values())
})
@property @property
def connection_info(self) -> Dict[str, str]: def connection_info(self) -> Dict[str, str]:
@@ -490,6 +572,45 @@ class NewareBatteryTestSystem:
def debug_resource_names(self) -> dict:
"""
调试方法显示所有资源的实际名称ROS2动作
Returns:
dict: ROS2动作结果格式包含所有资源名称信息
"""
try:
debug_info = {
"total_resources": len(self.station_resources),
"plate1_resources": len(self.station_resources_plate1),
"plate2_resources": len(self.station_resources_plate2),
"plate1_names": list(self.station_resources_plate1.keys())[:10], # 显示前10个
"plate2_names": list(self.station_resources_plate2.keys())[:10], # 显示前10个
"all_resource_names": list(self.station_resources.keys())[:20], # 显示前20个
}
# 检查是否有用户提到的命名格式
batterytestposition_names = [name for name in self.station_resources.keys()
if "batterytestposition" in name]
debug_info["batterytestposition_names"] = batterytestposition_names[:10]
success_msg = f"资源调试信息获取成功,共{debug_info['total_resources']}个资源"
if self._ros_node:
self._ros_node.lab_logger().info(success_msg)
self._ros_node.lab_logger().info(f"调试信息: {debug_info}")
return {
"return_info": success_msg,
"success": True,
"debug_data": debug_info
}
except Exception as e:
error_msg = f"获取资源调试信息失败: {str(e)}"
if self._ros_node:
self._ros_node.lab_logger().error(error_msg)
return {"return_info": error_msg, "success": False}
# ======================== # ========================
# 辅助方法 # 辅助方法
# ======================== # ========================
@@ -538,6 +659,228 @@ class NewareBatteryTestSystem:
except Exception as e: except Exception as e:
print(f" 获取状态失败: {e}") print(f" 获取状态失败: {e}")
# ========================
# CSV批量提交功能新增
# ========================
def _ensure_local_import_path(self):
"""确保本地模块导入路径"""
base_dir = os.path.dirname(__file__)
if base_dir not in sys.path:
sys.path.insert(0, base_dir)
def _canon(self, bs: str) -> str:
"""规范化电池体系名称"""
return str(bs).strip().replace('-', '_').upper()
def _compute_values(self, row):
"""
计算活性物质质量和容量
Args:
row: DataFrame行数据
Returns:
tuple: (活性物质质量mg, 容量mAh)
"""
pw = float(row['Pole_Weight'])
cm = float(row['集流体质量'])
am = row['活性物质含量']
if isinstance(am, str) and am.endswith('%'):
amv = float(am.rstrip('%')) / 100.0
else:
amv = float(am)
act_mass = (pw - cm) * amv
sc = float(row['克容量mah/g'])
cap = act_mass * sc / 1000.0
return round(act_mass, 2), round(cap, 3)
def _get_xml_builder(self, gen_mod, key: str):
"""
获取对应电池体系的XML生成函数
Args:
gen_mod: generate_xml_content模块
key: 电池体系标识
Returns:
callable: XML生成函数
"""
fmap = {
'LB6': gen_mod.xml_LB6,
'GR_LI': gen_mod.xml_Gr_Li,
'LFP_LI': gen_mod.xml_LFP_Li,
'LFP_GR': gen_mod.xml_LFP_Gr,
'811_LI_002': gen_mod.xml_811_Li_002,
'811_LI_005': gen_mod.xml_811_Li_005,
'SIGR_LI_STEP': gen_mod.xml_SiGr_Li_Step,
'SIGR_LI': gen_mod.xml_SiGr_Li_Step,
'811_SIGR': gen_mod.xml_811_SiGr,
}
if key not in fmap:
raise ValueError(f"未定义电池体系映射: {key}")
return fmap[key]
def _save_xml(self, xml: str, path: str):
"""
保存XML文件
Args:
xml: XML内容
path: 文件路径
"""
with open(path, 'w', encoding='utf-8') as f:
f.write(xml)
def submit_from_csv(self, csv_path: str, output_dir: str = ".") -> dict:
"""
从CSV文件批量提交Neware测试任务设备动作
Args:
csv_path (str): 输入CSV文件路径
output_dir (str): 输出目录用于存储XML文件和备份默认当前目录
Returns:
dict: 执行结果 {"return_info": str, "success": bool, "submitted_count": int}
"""
try:
# 确保可以导入本地模块
self._ensure_local_import_path()
import pandas as pd
import generate_xml_content as gen_mod
from neware_driver import start_test
if self._ros_node:
self._ros_node.lab_logger().info(f"开始从CSV文件提交任务: {csv_path}")
# 读取CSV文件
if not os.path.exists(csv_path):
error_msg = f"CSV文件不存在: {csv_path}"
if self._ros_node:
self._ros_node.lab_logger().error(error_msg)
return {"return_info": error_msg, "success": False, "submitted_count": 0}
df = pd.read_csv(csv_path, encoding='gbk')
# 验证必需列
required = [
'Battery_Code', 'Pole_Weight', '集流体质量', '活性物质含量',
'克容量mah/g', '电池体系', '设备号', '排号', '通道号'
]
missing = [c for c in required if c not in df.columns]
if missing:
error_msg = f"CSV缺少必需列: {missing}"
if self._ros_node:
self._ros_node.lab_logger().error(error_msg)
return {"return_info": error_msg, "success": False, "submitted_count": 0}
# 创建输出目录
xml_dir = os.path.join(output_dir, 'xml_dir')
backup_dir = os.path.join(output_dir, 'backup_dir')
os.makedirs(xml_dir, exist_ok=True)
os.makedirs(backup_dir, exist_ok=True)
if self._ros_node:
self._ros_node.lab_logger().info(
f"输出目录: XML={xml_dir}, 备份={backup_dir}"
)
# 逐行处理CSV数据
submitted_count = 0
results = []
for idx, row in df.iterrows():
try:
coin_id = str(row['Battery_Code'])
# 计算活性物质质量和容量
act_mass, cap_mAh = self._compute_values(row)
if cap_mAh < 0:
error_msg = (
f"容量为负数: Battery_Code={coin_id}, "
f"活性物质质量mg={act_mass}, 容量mah={cap_mAh}"
)
if self._ros_node:
self._ros_node.lab_logger().warning(error_msg)
results.append(f"{idx+1} 失败: {error_msg}")
continue
# 获取电池体系对应的XML生成函数
key = self._canon(row['电池体系'])
builder = self._get_xml_builder(gen_mod, key)
# 生成XML内容
xml_content = builder(act_mass, cap_mAh)
# 获取设备信息
devid = int(row['设备号'])
subdevid = int(row['排号'])
chlid = int(row['通道号'])
# 保存XML文件
recipe_path = os.path.join(
xml_dir,
f"{coin_id}_{devid}_{subdevid}_{chlid}.xml"
)
self._save_xml(xml_content, recipe_path)
# 提交测试任务
resp = start_test(
ip=self.ip,
port=self.port,
devid=devid,
subdevid=subdevid,
chlid=chlid,
CoinID=coin_id,
recipe_path=recipe_path,
backup_dir=backup_dir
)
submitted_count += 1
results.append(f"{idx+1} {coin_id}: {resp}")
if self._ros_node:
self._ros_node.lab_logger().info(
f"已提交 {coin_id} (设备{devid}-{subdevid}-{chlid}): {resp}"
)
except Exception as e:
error_msg = f"{idx+1} 处理失败: {str(e)}"
results.append(error_msg)
if self._ros_node:
self._ros_node.lab_logger().error(error_msg)
# 汇总结果
success_msg = (
f"批量提交完成: 成功{submitted_count}个,共{len(df)}行。"
f"\n详细结果:\n" + "\n".join(results)
)
if self._ros_node:
self._ros_node.lab_logger().info(
f"批量提交完成: 成功{submitted_count}/{len(df)}"
)
return {
"return_info": success_msg,
"success": True,
"submitted_count": submitted_count,
"total_count": len(df),
"results": results
}
except Exception as e:
error_msg = f"批量提交失败: {str(e)}"
if self._ros_node:
self._ros_node.lab_logger().error(error_msg)
return {
"return_info": error_msg,
"success": False,
"submitted_count": 0
}
def get_device_summary(self) -> dict: def get_device_summary(self) -> dict:
""" """
获取设备级别的摘要统计设备动作 获取设备级别的摘要统计设备动作

View File

@@ -0,0 +1,49 @@
import socket
END_MARKS = [b"\r\n#\r\n", b"</bts>"] # 读到任一标志即可判定完整响应
def build_start_command(devid, subdevid, chlid, CoinID,
ip_in_xml="127.0.0.1",
devtype:int=27,
recipe_path:str=f"D:\\HHM_test\\A001.xml",
backup_dir:str=f"D:\\HHM_test\\backup") -> str:
lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<bts version="1.0">',
' <cmd>start</cmd>',
' <list count="1">',
f' <start ip="{ip_in_xml}" devtype="{devtype}" devid="{devid}" subdevid="{subdevid}" chlid="{chlid}" barcode="{CoinID}">{recipe_path}</start>',
f' <backup backupdir="{backup_dir}" remotedir="" filenametype="1" customfilename="" createdirbydate="0" filetype="0" backupontime="1" backupontimeinterval="1" backupfree="0" />',
' </list>',
'</bts>',
]
# TCP 模式:请求必须以 #\r\n 结束(协议要求)
return "\r\n".join(lines) + "\r\n#\r\n"
def recv_until_marks(sock: socket.socket, timeout=60):
sock.settimeout(timeout) # 上限给足,协议允许到 30s:contentReference[oaicite:2]{index=2}
buf = bytearray()
while True:
chunk = sock.recv(8192)
if not chunk:
break
buf += chunk
# 读到结束标志就停,避免等对端断开
for m in END_MARKS:
if m in buf:
return bytes(buf)
# 保险:读到完整 XML 结束标签也停
if b"</bts>" in buf:
return bytes(buf)
return bytes(buf)
def start_test(ip="127.0.0.1", port=502, devid=3, subdevid=2, chlid=1, CoinID="A001", recipe_path=f"D:\\HHM_test\\A001.xml", backup_dir=f"D:\\HHM_test\\backup"):
xml_cmd = build_start_command(devid=devid, subdevid=subdevid, chlid=chlid, CoinID=CoinID, recipe_path=recipe_path, backup_dir=backup_dir)
#print(xml_cmd)
with socket.create_connection((ip, port), timeout=60) as s:
s.sendall(xml_cmd.encode("utf-8"))
data = recv_until_marks(s, timeout=60)
return data.decode("utf-8", errors="replace")
if __name__ == "__main__":
resp = start_test(ip="127.0.0.1", port=502, devid=4, subdevid=10, chlid=1, CoinID="A001", recipe_path=f"D:\\HHM_test\\A001.xml", backup_dir=f"D:\\HHM_test\\backup")
print(resp)

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from cgi import print_arguments from cgi import print_arguments
from doctest import debug from doctest import debug
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional, Tuple
import requests import requests
from pylabrobot.resources.resource import Resource as ResourcePLR from pylabrobot.resources.resource import Resource as ResourcePLR
from pathlib import Path from pathlib import Path
@@ -11,14 +11,30 @@ from datetime import datetime, timedelta
import re import re
import threading import threading
import json import json
from copy import deepcopy
from urllib3 import response from urllib3 import response
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation, BioyondResourceSynchronizer from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation, BioyondResourceSynchronizer
from unilabos.devices.workstation.bioyond_studio.config import ( from unilabos.devices.workstation.bioyond_studio.config import (
API_CONFIG, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, SOLID_LIQUID_MAPPINGS API_CONFIG, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, SOLID_LIQUID_MAPPINGS
) )
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
from unilabos.resources.bioyond.decks import BIOYOND_YB_Deck
from unilabos.resources.graphio import resource_bioyond_to_plr
from unilabos.utils.log import logger from unilabos.utils.log import logger
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
class device(BIOYOND_YB_Deck):
@classmethod
def deserialize(cls, data, allow_marshal=False): # type: ignore[override]
patched = dict(data)
if patched.get("type") == "device":
patched["type"] = "Deck"
if patched.get("category") == "device":
patched["category"] = "deck"
return super().deserialize(patched, allow_marshal=allow_marshal)
def _iso_local_now_ms() -> str: def _iso_local_now_ms() -> str:
# 文档要求:到毫秒 + Z例如 2025-08-15T05:43:22.814Z # 文档要求:到毫秒 + Z例如 2025-08-15T05:43:22.814Z
@@ -38,12 +54,14 @@ class BioyondCellWorkstation(BioyondWorkstation):
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs): def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
# 使用统一配置,支持自定义覆盖, 从 config.py 加载完整配置 # 使用统一配置,支持自定义覆盖, 从 config.py 加载完整配置
self.bioyond_config ={ self.bioyond_config = {
**API_CONFIG, **API_CONFIG,
"material_type_mappings": MATERIAL_TYPE_MAPPINGS, "material_type_mappings": MATERIAL_TYPE_MAPPINGS,
"warehouse_mapping": WAREHOUSE_MAPPING, "warehouse_mapping": WAREHOUSE_MAPPING,
"debug_mode": False "debug_mode": False,
} }
if config:
self.bioyond_config.update(config)
# "material_type_mappings": MATERIAL_TYPE_MAPPINGS # "material_type_mappings": MATERIAL_TYPE_MAPPINGS
# "warehouse_mapping": WAREHOUSE_MAPPING # "warehouse_mapping": WAREHOUSE_MAPPING
@@ -54,6 +72,12 @@ class BioyondCellWorkstation(BioyondWorkstation):
self.http_service_started = self.debug_mode self.http_service_started = self.debug_mode
self._device_id = "bioyond_cell_workstation" # 默认值后续会从_ros_node获取 self._device_id = "bioyond_cell_workstation" # 默认值后续会从_ros_node获取
super().__init__(bioyond_config=config, deck=deck) super().__init__(bioyond_config=config, deck=deck)
self.transfer_target_device_id = self.bioyond_config.get("transfer_target_device_id", "BatteryStation")
self.transfer_target_parent = self.bioyond_config.get("transfer_target_parent", "YB_YH_Deck")
self.transfer_timeout = float(self.bioyond_config.get("transfer_timeout", 180.0))
self.coin_cell_workflow_config = self.bioyond_config.get("coin_cell_workflow_config", {})
self.pending_transfer_materials: List[Dict[str, Any]] = []
self.pending_transfer_plr: List[ResourcePLR] = []
self.update_push_ip() #直接修改奔耀端的报送ip地址 self.update_push_ip() #直接修改奔耀端的报送ip地址
logger.info("已更新奔耀端推送 IP 地址") logger.info("已更新奔耀端推送 IP 地址")
@@ -322,6 +346,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
"posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]), "posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]),
"materialName": str(row[5]).strip(), "materialName": str(row[5]).strip(),
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0, "quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
"temperature": 0,
}) })
# 四号手套箱原液瓶面 # 四号手套箱原液瓶面
for _, row in df.iloc[14:23, 2:9].iterrows(): for _, row in df.iloc[14:23, 2:9].iterrows():
@@ -333,6 +358,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0, "quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
"materialType": str(row[7]).strip() if pd.notna(row[7]) else "", "materialType": str(row[7]).strip() if pd.notna(row[7]) else "",
"targetWH": str(row[8]).strip() if pd.notna(row[8]) else "", "targetWH": str(row[8]).strip() if pd.notna(row[8]) else "",
"temperature": 0,
}) })
# 三号手套箱人工堆栈 # 三号手套箱人工堆栈
for _, row in df.iloc[25:40, 2:7].iterrows(): for _, row in df.iloc[25:40, 2:7].iterrows():
@@ -342,11 +368,12 @@ class BioyondCellWorkstation(BioyondWorkstation):
"posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]), "posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]),
"materialType": str(row[5]).strip() if pd.notna(row[5]) else "", "materialType": str(row[5]).strip() if pd.notna(row[5]) else "",
"materialId": str(row[6]).strip() if pd.notna(row[6]) else "", "materialId": str(row[6]).strip() if pd.notna(row[6]) else "",
"quantity": 1 "quantity": 1,
"temperature": 0,
}) })
else: else:
logger.warning(f"未找到 Excel 文件 {xlsx_path},自动切换到手动参数模式。") logger.warning(f"未找到 Excel 文件 {xlsx_path},自动切换到手动参数模式。")
# TODO: 温度下面手动模式没改,上面的改了
# ---------- 模式 2: 手动填写 ---------- # ---------- 模式 2: 手动填写 ----------
if not items: if not items:
params = locals() params = locals()
@@ -389,10 +416,14 @@ class BioyondCellWorkstation(BioyondWorkstation):
order_code = response.get("data", {}).get("orderCode") order_code = response.get("data", {}).get("orderCode")
if not order_code: if not order_code:
logger.error("上料任务未返回有效 orderCode") logger.error("上料任务未返回有效 orderCode")
return response return {"api_response": response, "order_finish": None}
# 等待完成报送 # 等待完成报送
result = self.wait_for_order_finish(order_code) result = self.wait_for_order_finish(order_code)
return result return {
"api_response": response,
"order_finish": result,
"items": items,
}
def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]: def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
@@ -463,7 +494,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
return response return response
# 2.14 新建实验 # 2.14 新建实验
def create_orders(self, xlsx_path: str) -> Dict[str, Any]: def create_orders(self, xlsx_path: str, *, material_filter: Optional[str] = None) -> Dict[str, Any]:
""" """
从 Excel 解析并创建实验2.14 从 Excel 解析并创建实验2.14
约定: 约定:
@@ -543,6 +574,14 @@ class BioyondCellWorkstation(BioyondWorkstation):
except Exception: except Exception:
return default return default
def _as_float(val, default=0.0) -> float:
try:
if pd.isna(val):
return default
return float(val)
except Exception:
return default
def _as_str(val, default="") -> str: def _as_str(val, default="") -> str:
if val is None or (isinstance(val, float) and pd.isna(val)): if val is None or (isinstance(val, float) and pd.isna(val)):
return default return default
@@ -576,9 +615,9 @@ class BioyondCellWorkstation(BioyondWorkstation):
"createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None), "createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None),
"bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶", "bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶",
"mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0, "mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0,
"loadSheddingInfo": _as_int(row[col_load]) if col_load else 0, "loadSheddingInfo": _as_float(row[col_load]) if col_load else 0.0,
"pouchCellInfo": _as_int(row[col_pouch]) if col_pouch else 0, "pouchCellInfo": _as_float(row[col_pouch]) if col_pouch else 0,
"conductivityInfo": _as_int(row[col_cond]) if col_cond else 0, "conductivityInfo": _as_float(row[col_cond]) if col_cond else 0,
"conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0, "conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0,
"materialInfos": mats, "materialInfos": mats,
"totalMass": round(total_mass, 4) # 自动汇总 "totalMass": round(total_mass, 4) # 自动汇总
@@ -594,7 +633,8 @@ class BioyondCellWorkstation(BioyondWorkstation):
print(f"[create_orders] ⚠️ 第 {idx+1} 行未找到有效物料") print(f"[create_orders] ⚠️ 第 {idx+1} 行未找到有效物料")
orders.append(order_data) orders.append(order_data)
print("================================================")
print("orders:", orders)
print(f"[create_orders] 即将提交订单数量: {len(orders)}") print(f"[create_orders] 即将提交订单数量: {len(orders)}")
response = self._post_lims("/api/lims/order/orders", orders) response = self._post_lims("/api/lims/order/orders", orders)
@@ -609,9 +649,36 @@ class BioyondCellWorkstation(BioyondWorkstation):
if not order_code: if not order_code:
logger.error("上料任务未返回有效 orderCode") logger.error("上料任务未返回有效 orderCode")
return response return response
# 等待完成报送 # 等待完成报送
result = self.wait_for_order_finish(order_code) result = self.wait_for_order_finish(order_code)
return result report_data = result.get("report") if isinstance(result, dict) else None
materials_from_report = (
report_data.get("usedMaterials") if isinstance(report_data, dict) else None
)
if materials_from_report:
materials = materials_from_report
logger.info(
"[create_orders] 使用订单完成报送中的物料信息: "
f"{len(materials)}"
)
else:
materials = self._fetch_bioyond_materials(filter_keyword=material_filter)
logger.info(
"[create_orders] 未收到订单报送物料信息,回退到实时查询"
)
print("materials_from_report:", materials_from_report)
# TODO: 需要将 materials 字典转换为 ResourceSlot 对象后才能转运
# self.transfer_resource_to_another(
# resource=[materials],
# mount_resource=["YB_YH_Deck"],
# sites=[None],
# mount_device_id="BatteryStation"
# )
return {
"api_response": response,
"order_finish": result,
"materials": materials,
}
# 2.7 启动调度 # 2.7 启动调度
def scheduler_start(self) -> Dict[str, Any]: def scheduler_start(self) -> Dict[str, Any]:
@@ -683,6 +750,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
return response return response
# 等待完成报送 # 等待完成报送
result = self.wait_for_order_finish(order_code) result = self.wait_for_order_finish(order_code)
return result return result
# 2.5 批量查询实验报告(post过滤关键字查询) # 2.5 批量查询实验报告(post过滤关键字查询)
@@ -1075,7 +1143,12 @@ class BioyondCellWorkstation(BioyondWorkstation):
if bottle_moudle == moudle_name: if bottle_moudle == moudle_name:
bottle_type = key bottle_type = key
break break
self.create_sample(plr_resource.name, board_type,bottle_type,site)
# 从 parent_resource 获取仓库名称
warehouse_name = parent_resource.name if parent_resource else "手动堆栈"
logger.info(f"拖拽上料: {plr_resource.name} -> {warehouse_name} / {site}")
self.create_sample(plr_resource.name, board_type, bottle_type, site, warehouse_name)
return return
self.lab_logger().warning(f"无库位的上料,不处理,{plr_resource} 挂载到 {parent_resource}") self.lab_logger().warning(f"无库位的上料,不处理,{plr_resource} 挂载到 {parent_resource}")
@@ -1084,17 +1157,31 @@ class BioyondCellWorkstation(BioyondWorkstation):
name: str, name: str,
board_type: str, board_type: str,
bottle_type: str, bottle_type: str,
location_code: str location_code: str,
warehouse_name: str = "手动堆栈"
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""创建配液板物料并自动入库。 """创建配液板物料并自动入库。
Args: Args:
material_name: 物料名称,支持 "5ml分液瓶板"/"5ml分液瓶""配液瓶(小)板"/"配液瓶(小)" name: 物料名称
quantity: 主物料与明细的数量,默认 1。 board_type: 板类型,如 "5ml分液瓶板""配液瓶(小)板"
location_code: 库位编号,例如 "A01",将自动映射为 "手动堆栈" 下的 UUID。 bottle_type: 瓶类型,如 "5ml分液瓶""配液瓶(小)"
location_code: 库位编号,例如 "A01"
warehouse_name: 仓库名称,默认为 "手动堆栈",支持 "自动堆栈-左""自动堆栈-右"
""" """
carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1] carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1]
bottle_type_id = MATERIAL_TYPE_MAPPINGS[bottle_type][1] bottle_type_id = MATERIAL_TYPE_MAPPINGS[bottle_type][1]
location_id = WAREHOUSE_MAPPING["手动堆栈"]["site_uuids"][location_code]
# 从指定仓库获取库位UUID
if warehouse_name not in WAREHOUSE_MAPPING:
logger.error(f"未找到仓库: {warehouse_name},回退到手动堆栈")
warehouse_name = "手动堆栈"
if location_code not in WAREHOUSE_MAPPING[warehouse_name]["site_uuids"]:
logger.error(f"仓库 {warehouse_name} 中未找到库位 {location_code}")
raise ValueError(f"库位 {location_code} 在仓库 {warehouse_name} 中不存在")
location_id = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"][location_code]
logger.info(f"创建样品入库: {name} -> {warehouse_name}/{location_code} (UUID: {location_id})")
# 新建小瓶 # 新建小瓶
details = [] details = []
@@ -1132,33 +1219,221 @@ class BioyondCellWorkstation(BioyondWorkstation):
}) })
return final_result return final_result
def _fetch_bioyond_materials(
self,
*,
filter_keyword: Optional[str] = None,
type_mode: int = 2,
) -> List[Dict[str, Any]]:
query: Dict[str, Any] = {
"typeMode": type_mode,
"includeDetail": True,
}
if filter_keyword:
query["filter"] = filter_keyword
response = self._post_lims("/api/lims/storage/stock-material", query)
raw_materials = response.get("data")
if not isinstance(raw_materials, list):
raw_materials = []
try:
resource_bioyond_to_plr(
raw_materials,
type_mapping=self.bioyond_config.get("material_type_mappings", MATERIAL_TYPE_MAPPINGS),
deck=self.deck,
)
except Exception as exc:
logger.warning(f"转换奔曜物料到 PLR 失败: {exc}", exc_info=True)
return raw_materials
def _convert_materials_to_plr(self, materials: List[Dict[str, Any]]) -> List[ResourcePLR]:
try:
return resource_bioyond_to_plr(
deepcopy(materials),
type_mapping=self.bioyond_config.get("material_type_mappings", MATERIAL_TYPE_MAPPINGS),
deck=self.deck,
)
except Exception as exc:
logger.error(f"物料转换为 PLR 失败: {exc}", exc_info=True)
return []
def _wait_for_future(self, future, stage: str, timeout: Optional[float] = None):
if future is None:
return None
timeout = timeout or self.transfer_timeout
start = time.time()
while not future.done():
if (time.time() - start) > timeout:
raise TimeoutError(f"{stage} 超时 {timeout}s")
time.sleep(0.05)
return future.result()
def _register_plr_resources(self, resources: List[ResourcePLR]) -> None:
if not resources or not hasattr(self, "_ros_node") or self._ros_node is None:
return
future = ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, resources=resources)
self._wait_for_future(future, "update_resource")
def _get_target_resource(self, name: str) -> ResourcePLR:
if not hasattr(self, "_ros_node") or self._ros_node is None:
raise RuntimeError("ROS 节点未初始化,无法获取资源")
resource = self._ros_node.resource_tracker.figure_resource({"name": name}, try_mode=False) # type: ignore
if resource is None:
raise ValueError(f"未找到目标资源: {name}")
return resource
def _allocate_sites(self, parent_resource: ResourcePLR, count: int) -> List[str]:
if not hasattr(parent_resource, "get_free_sites"):
raise ValueError(f"资源 {parent_resource} 不支持自动分配站位")
free_indices = list(parent_resource.get_free_sites())
if len(free_indices) < count:
raise ValueError(f"{parent_resource.name} 可用站位不足 (need {count}, have {len(free_indices)})")
ordering = list(getattr(parent_resource, "_ordering", {}).keys())
sites: List[str] = []
for idx in free_indices[:count]:
if ordering and idx < len(ordering):
sites.append(ordering[idx])
else:
sites.append(str(idx))
return sites
def _invoke_coin_cell_workflow(self, material_payload: List[Dict[str, Any]]) -> Any:
timeout = float(self.bioyond_config.get("coin_cell_workflow_timeout", 300.0))
workflow_payload: Dict[str, Any] = {}
if isinstance(self.coin_cell_workflow_config, dict):
workflow_payload.update(deepcopy(self.coin_cell_workflow_config))
workflow_payload["materials"] = deepcopy(material_payload)
return self._call_remote_device_method(
self.transfer_target_device_id,
"run_coin_cell_assembly_workflow",
timeout=timeout,
workflow_config=workflow_payload,
)
def _call_remote_device_method(
self,
device_id: str,
method: str,
*,
timeout: Optional[float] = None,
**kwargs,
) -> Any:
if not hasattr(self, "_ros_node") or self._ros_node is None:
raise RuntimeError("ROS 节点未初始化,无法调用远程设备")
if not device_id:
raise ValueError("device_id 不能为空")
if not method:
raise ValueError("method 不能为空")
timeout = timeout or self.transfer_timeout
payload = json.dumps(
{
"function_name": method,
"function_args": kwargs,
},
ensure_ascii=False,
)
future = ROS2DeviceNode.run_async_func(
self._ros_node.execute_single_action,
True,
device_id=device_id,
action_name="_execute_driver_command_async",
action_kwargs={"string": payload},
)
result = self._wait_for_future(future, f"{device_id}.{method}", timeout)
if hasattr(result, "return_info"):
try:
return json.loads(result.return_info)
except Exception:
return result.return_info
return result
def run_feeding_stage(self) -> Dict[str, Any]:
self.create_sample(
board_type="配液瓶(小)板",
bottle_type="配液瓶(小)",
location_code="B01",
name="配液瓶",
warehouse_name="手动堆栈"
)
self.create_sample(
board_type="5ml分液瓶板",
bottle_type="5ml分液瓶",
location_code="B02",
name="分液瓶",
warehouse_name="手动堆栈"
)
self.scheduler_start()
feeding_task = self.auto_feeding4to3(
xlsx_path="/Users/sml/work/Unilab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx"
)
feeding_materials = self._fetch_bioyond_materials()
return {
"feeding_materials": feeding_materials,
"feeding_items": feeding_task.get("items", []),
"feeding_task": feeding_task,
}
def run_liquid_preparation_stage(
self,
feeding_materials: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, List[Dict[str, Any]]]:
result = self.create_orders(
xlsx_path="/Users/sml/work/Unilab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/2025092701.xlsx"
)
filter_keyword = self.bioyond_config.get("mixing_material_filter") or None
materials = result.get("materials")
if materials is None:
materials = self._fetch_bioyond_materials(filter_keyword=filter_keyword)
return {
"feeding_materials": feeding_materials or [],
"liquid_materials": materials,
}
def run_transfer_stage(
self,
liquid_materials: Optional[List[Dict[str, Any]]] = None,
source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b',
source_x: int = 1,
source_y: int = 1,
source_z: int = 1
) -> Dict[str, Any]:
"""转运阶段调用transfer_3_to_2_to_1执行3到2到1转运"""
logger.info("开始执行转运阶段 (run_transfer_stage)")
# 暂时注释掉物料转换和跨工站转运逻辑
# transfer_summary: Dict[str, Any] = {}
# try:
# source_materials = liquid_materials or self._fetch_bioyond_materials()
# transfer_plr = self._convert_materials_to_plr(source_materials)
# transfer_summary["plr_count"] = len(transfer_plr)
# ...
# except Exception as exc:
# transfer_summary["error"] = str(exc)
# logger.error(f"跨工站转运失败: {exc}", exc_info=True)
# 只执行核心的3到2到1转运
transfer_result = self.transfer_3_to_2_to_1(
source_wh_id=source_wh_id,
source_x=source_x,
source_y=source_y,
source_z=source_z
)
logger.info("转运阶段执行完成")
return {
"success": True,
"stage": "transfer",
"transfer_result": transfer_result
}
if __name__ == "__main__": if __name__ == "__main__":
lab_registry.setup() deck = BIOYOND_YB_Deck(setup=True)
ws = BioyondCellWorkstation() w = BioyondCellWorkstation(deck=deck, address="172.16.28.102", port="502", debug_mode=False)
# ws.create_sample(name="test", board_type="配液瓶(小)板", bottle_type="配液瓶(小)", location_code="B01") feeding = w.run_feeding_stage()
# logger.info(ws.scheduler_stop()) liquid = w.run_liquid_preparation_stage(feeding.get("feeding_materials"))
# logger.info(ws.scheduler_start()) transfer = w.run_transfer_stage(liquid.get("liquid_materials"))
# results = ws.create_materials(SOLID_LIQUID_MAPPINGS)
# for r in results:
# logger.info(r)
# 从CSV文件读取物料列表并批量创建入库
# result = ws.create_and_inbound_materials()
# 继续后续流程
# logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱
# # # 使用正斜杠或 Path 对象来指定文件路径
# excel_path = Path("unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\2025092701.xlsx")
# logger.info(ws.create_orders(excel_path))
# logger.info(ws.transfer_3_to_2_to_1())
# logger.info(ws.transfer_1_to_2())
# logger.info(ws.scheduler_start())
while True: while True:
time.sleep(1) time.sleep(1)
# re=ws.scheduler_stop() # re=ws.scheduler_stop()

View File

@@ -113,7 +113,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine_four", "type": "MagazineHolder_4",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -154,7 +154,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -249,7 +249,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -344,7 +344,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -439,7 +439,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -537,7 +537,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine_four", "type": "MagazineHolder_4",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -578,7 +578,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -673,7 +673,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -768,7 +768,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -863,7 +863,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -963,7 +963,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -1006,7 +1006,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1101,7 +1101,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1196,7 +1196,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1291,7 +1291,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1386,7 +1386,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1481,7 +1481,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1581,7 +1581,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -1624,7 +1624,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1719,7 +1719,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1814,7 +1814,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -1909,7 +1909,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2004,7 +2004,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2099,7 +2099,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2199,7 +2199,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -2242,7 +2242,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2337,7 +2337,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2432,7 +2432,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2527,7 +2527,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2622,7 +2622,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2717,7 +2717,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2817,7 +2817,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -2860,7 +2860,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -2955,7 +2955,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3050,7 +3050,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3145,7 +3145,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3240,7 +3240,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3335,7 +3335,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3435,7 +3435,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -3478,7 +3478,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3573,7 +3573,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3668,7 +3668,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3763,7 +3763,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3858,7 +3858,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -3953,7 +3953,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4053,7 +4053,7 @@
"z": 0 "z": 0
}, },
"config": { "config": {
"type": "ClipMagazine", "type": "MagazineHolder_6",
"size_x": 80, "size_x": 80,
"size_y": 80, "size_y": 80,
"size_z": 10, "size_z": 10,
@@ -4096,7 +4096,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4191,7 +4191,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4286,7 +4286,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4381,7 +4381,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4476,7 +4476,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,
@@ -4571,7 +4571,7 @@
"z": 10 "z": 10
}, },
"config": { "config": {
"type": "ClipMagazineHole", "type": "Magazine",
"size_x": 14.0, "size_x": 14.0,
"size_y": 14.0, "size_y": 14.0,
"size_z": 10.0, "size_z": 10.0,

View File

@@ -9,7 +9,7 @@ import os
API_CONFIG = { API_CONFIG = {
# API 连接配置 # API 连接配置
# "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.1.143:44389"),#实机 # "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.1.143:44389"),#实机
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.7.149:44388"),# 仿真机 "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.11.219:44388"),# 仿真机
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"), "api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")), "timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")),
@@ -17,7 +17,7 @@ API_CONFIG = {
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"), "report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
# HTTP 服务配置 # HTTP 服务配置
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.16.2.140"), # HTTP服务监听地址监听计算机飞连ip地址 "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.16.11.2"), # HTTP服务监听地址监听计算机飞连ip地址
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")), "HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
"debug_mode": False,# 调试模式 "debug_mode": False,# 调试模式
} }
@@ -237,7 +237,7 @@ MATERIAL_TYPE_MAPPINGS = {
"100ml液体": ("YB_100ml_yeti", "d37166b3-ecaa-481e-bd84-3032b795ba07"), "100ml液体": ("YB_100ml_yeti", "d37166b3-ecaa-481e-bd84-3032b795ba07"),
"": ("YB_ye", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"), "": ("YB_ye", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
"高粘液": ("YB_gaonianye", "abe8df30-563d-43d2-85e0-cabec59ddc16"), "高粘液": ("YB_gaonianye", "abe8df30-563d-43d2-85e0-cabec59ddc16"),
"加样头(大)": ("YB_jia_yang_tou_da", "3a190ca0-b2f6-9aeb-8067-547e72c11469"), "加样头(大)": ("YB_jia_yang_tou_da_Carrier", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "加样头(大)板": ("YB_jia_yang_tou_da", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"), # "加样头(大)板": ("YB_jia_yang_tou_da", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"),
"5ml分液瓶板": ("YB_5ml_fenyepingban", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"), "5ml分液瓶板": ("YB_5ml_fenyepingban", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"),
"5ml分液瓶": ("YB_5ml_fenyeping", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"), "5ml分液瓶": ("YB_5ml_fenyeping", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"),

View File

@@ -18,67 +18,11 @@ from pylabrobot.resources.tip_rack import TipRack, TipSpot
from pylabrobot.resources.trash import Trash from pylabrobot.resources.trash import Trash
from pylabrobot.resources.utils import create_ordered_items_2d 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
class ElectrodeSheetState(TypedDict):
diameter: float # 直径 (mm)
thickness: float # 厚度 (mm)
mass: float # 质量 (g)
material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等)
height: float
electrolyte_name: str
data_electrolyte_code: str
open_circuit_voltage: float
assembly_pressure: float
electrolyte_volume: float
info: Optional[str] # 附加信息
class ElectrodeSheet(Resource):
"""极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料"""
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
# TODO: 这个应该只能放一个极片 # TODO: 这个应该只能放一个极片
class MaterialHoleState(TypedDict): class MaterialHoleState(TypedDict):
@@ -165,7 +109,6 @@ class MaterialHole(Resource):
return self.children[index] return self.children[index]
class MaterialPlateState(TypedDict): class MaterialPlateState(TypedDict):
hole_spacing_x: float hole_spacing_x: float
hole_spacing_y: float hole_spacing_y: float
@@ -327,132 +270,6 @@ class PlateSlot(ResourceStack):
} }
class ClipMagazineHole(Container):
"""子弹夹洞位类"""
def __init__(
self,
name: str,
diameter: float,
depth: float,
max_sheets: int = 100,
category: str = "clip_magazine_hole",
):
"""初始化子弹夹洞位
Args:
name: 洞位名称
diameter: 洞直径 (mm)
depth: 洞深度 (mm)
max_sheets: 最大极片数量
category: 类别
"""
super().__init__(
name=name,
size_x=diameter,
size_y=diameter,
size_z=depth,
category=category,
)
self.diameter = diameter
self.depth = depth
self.max_sheets = max_sheets
self._sheets: List[ElectrodeSheet] = []
def can_add_sheet(self, sheet: ElectrodeSheet) -> bool:
"""检查是否可以添加极片"""
return (len(self._sheets) < self.max_sheets and
sheet.diameter <= self.diameter)
def add_sheet(self, sheet: ElectrodeSheet) -> None:
"""添加极片"""
if not self.can_add_sheet(sheet):
raise ValueError(f"无法向洞位 {self.name} 添加极片")
self._sheets.append(sheet)
def take_sheet(self) -> ElectrodeSheet:
"""取出极片"""
if len(self._sheets) == 0:
raise ValueError(f"洞位 {self.name} 没有极片")
return self._sheets.pop()
def get_sheet_count(self) -> int:
"""获取极片数量"""
return len(self._sheets)
def serialize_state(self) -> Dict[str, Any]:
return {
"sheet_count": len(self._sheets),
"sheets": [sheet.serialize() for sheet in self._sheets],
}
# TODO: 这个要改
class ClipMagazine(ItemizedResource[ClipMagazineHole]):
"""子弹夹类 - 有6个洞位每个洞位放多个极片"""
children: List[ClipMagazineHole]
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 25.0,
max_sheets_per_hole: int = 100,
category: str = "clip_magazine",
model: Optional[str] = None,
):
"""初始化子弹夹
Args:
name: 子弹夹名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing: 洞位间距 (mm)
max_sheets_per_hole: 每个洞位最大极片数量
category: 类别
model: 型号
"""
# 创建6个洞位排成2x3布局
holes = create_ordered_items_2d(
klass=ClipMagazineHole,
num_items_x=3,
num_items_y=2,
dx=(size_x - 2 * hole_spacing) / 2, # 居中
dy=(size_y - hole_spacing) / 2, # 居中
dz=size_z - 0,
item_dx=hole_spacing,
item_dy=hole_spacing,
diameter=hole_diameter,
depth=hole_depth,
)
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
# 保存洞位的直径和深度
self.hole_diameter = hole_diameter
self.hole_depth = hole_depth
self.max_sheets_per_hole = max_sheets_per_hole
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,
}
#是一种类型注解不用self #是一种类型注解不用self
class BatteryState(TypedDict): class BatteryState(TypedDict):
"""电池状态字典""" """电池状态字典"""
@@ -595,76 +412,56 @@ class BatteryPressSlot(Resource):
def get_battery_info(self, index: int) -> Battery: def get_battery_info(self, index: int) -> Battery:
return self.children[0] return self.children[0]
# TODO:这个移液枪架子看一下从哪继承
class TipBox64State(TypedDict):
"""电池状态字典"""
tip_diameter: float = 5.0
tip_length: float = 50.0
with_tips: bool = True
class TipBox64(TipRack): def TipBox64(
"""64孔枪头盒类"""
children: List[TipSpot] = []
def __init__(
self,
name: str, name: str,
size_x: float = 127.8, size_x: float = 127.8,
size_y: float = 85.5, size_y: float = 85.5,
size_z: float = 60.0, size_z: float = 60.0,
category: str = "tip_box_64", category: str = "tip_rack",
model: Optional[str] = None, model: Optional[str] = None,
): ):
"""初始化64孔枪头盒 """64孔枪头盒"""
from pylabrobot.resources.tip import Tip
Args: # 创建12x8=96个枪头位
name: 枪头盒名称 def make_tip():
size_x: 长度 (mm) return Tip(
size_y: 宽度 (mm) has_filter=False,
size_z: 高度 (mm) total_tip_length=20.0,
tip_diameter: 枪头直径 (mm) maximal_volume=1000, # 1mL
tip_length: 枪头长度 (mm) fitting_depth=8.0,
category: 类别
model: 型号
with_tips: 是否带枪头
"""
from pylabrobot.resources.tip import Tip
# 创建8x8=64个枪头位
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=8,
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,
)
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=tip_spots,
category=category,
model=model,
with_tips=True,
) )
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): class WasteTipBoxstate(TypedDict):
@@ -682,8 +479,12 @@ class WasteTipBox(Trash):
size_x: float = 127.8, size_x: float = 127.8,
size_y: float = 85.5, size_y: float = 85.5,
size_z: float = 60.0, size_z: float = 60.0,
category: str = "waste_tip_box", material_z_thickness=0,
model: Optional[str] = None, max_volume=float("inf"),
category="trash",
model=None,
compute_volume_from_height=None,
compute_height_from_volume=None,
): ):
"""初始化废枪头盒 """初始化废枪头盒
@@ -733,263 +534,16 @@ class WasteTipBox(Trash):
return data return data
class BottleRackState(TypedDict):
""" bottle_diameter: 瓶子直径 (mm)
bottle_height: 瓶子高度 (mm)
position_spacing: 位置间距 (mm)"""
bottle_diameter: float
bottle_height: float
name_to_index: dict
class BottleRack(Resource):
"""瓶架类 - 12个待配位置+12个已配位置"""
children: List[Resource] = []
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str = "bottle_rack",
model: Optional[str] = None,
num_items_x: int = 3,
num_items_y: int = 4,
position_spacing: float = 35.0,
orientation: str = "horizontal",
padding_x: float = 20.0,
padding_y: float = 20.0,
):
"""初始化瓶架
Args:
name: 瓶架名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
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: BottleRackState = BottleRackState(
bottle_diameter=30.0,
bottle_height=100.0,
position_spacing=position_spacing,
name_to_index={},
)
# 基于网格生成瓶位坐标映射(居中摆放)
# 使用内边距避免点跑到容器外前端渲染不按mm等比缩放时更稳妥
origin_x = padding_x
origin_y = padding_y
self.index_to_pos = {}
for j in range(num_items_y):
for i in range(num_items_x):
idx = j * num_items_x + i
if orientation == "vertical":
# 纵向:沿 y 方向优先排列
self.index_to_pos[idx] = Coordinate(
x=origin_x + j * position_spacing,
y=origin_y + i * position_spacing,
z=0,
)
else:
# 横向(默认):沿 x 方向优先排列
self.index_to_pos[idx] = Coordinate(
x=origin_x + i * position_spacing,
y=origin_y + j * position_spacing,
z=0,
)
self.name_to_index = {}
self.name_to_pos = {}
self.num_items_x = num_items_x
self.num_items_y = num_items_y
self.orientation = orientation
self.padding_x = padding_x
self.padding_y = padding_y
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
# TODO: 这里有些问题要重新写一下
def assign_child_resource_old(self, resource: Resource, location=Coordinate.zero(), reassign=True):
capacity = self.num_items_x * self.num_items_y
assert len(self.children) < capacity, "瓶架已满,无法添加更多瓶子"
index = len(self.children)
location = self.index_to_pos.get(index, Coordinate.zero())
self.name_to_pos[resource.name] = location
self.name_to_index[resource.name] = index
return super().assign_child_resource(resource, location, reassign)
def assign_child_resource(self, resource: Resource, index: int):
capacity = self.num_items_x * self.num_items_y
assert 0 <= index < capacity, "无效的瓶子索引"
self.name_to_index[resource.name] = index
location = self.index_to_pos[index]
return super().assign_child_resource(resource, location)
def unassign_child_resource(self, resource: Bottle):
super().unassign_child_resource(resource)
self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None)
def serialize(self) -> dict:
return {
**super().serialize(),
"num_items_x": self.num_items_x,
"num_items_y": self.num_items_y,
"position_spacing": self._unilabos_state.get("position_spacing", 35.0),
"orientation": self.orientation,
"padding_x": self.padding_x,
"padding_y": self.padding_y,
}
class BottleState(TypedDict):
diameter: float
height: float
electrolyte_name: str
electrolyte_volume: float
max_volume: float
class Bottle(Resource):
"""瓶子类 - 容纳电解液"""
def __init__(
self,
name: str,
category: str = "bottle",
):
"""初始化瓶子
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: BottleState = BottleState()
def aspirate_electrolyte(self, volume: float) -> bool:
current_volume = self._unilabos_state["electrolyte_volume"]
assert current_volume > volume, f"Cannot aspirate {volume}μL, only {current_volume}μL available."
self._unilabos_state["electrolyte_volume"] -= volume
return True
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 ClipMagazine_four(ItemizedResource[ClipMagazineHole]):
"""子弹夹类 - 有4个洞位每个洞位放多个极片"""
children: List[ClipMagazineHole]
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 25.0,
max_sheets_per_hole: int = 100,
category: str = "clip_magazine_four",
model: Optional[str] = None,
):
"""初始化子弹夹
Args:
name: 子弹夹名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing: 洞位间距 (mm)
max_sheets_per_hole: 每个洞位最大极片数量
category: 类别
model: 型号
"""
# 创建4个洞位排成2x2布局
holes = create_ordered_items_2d(
klass=ClipMagazineHole,
num_items_x=2,
num_items_y=2,
dx=(size_x - 2 * hole_spacing) / 2, # 居中
dy=(size_y - hole_spacing) / 2, # 居中
dz=size_z - 0,
item_dx=hole_spacing,
item_dy=hole_spacing,
diameter=hole_diameter,
depth=hole_depth,
)
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
# 保存洞位的直径和深度
self.hole_diameter = hole_diameter
self.hole_depth = hole_depth
self.max_sheets_per_hole = max_sheets_per_hole
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,
}
class CoincellDeck(Deck): class CoincellDeck(Deck):
"""纽扣电池组装工作站台面类""" """纽扣电池组装工作站台面类"""
def __init__( def __init__(
self, self,
name: str = "coin_cell_deck", name: str = "coin_cell_deck",
size_x: float = 3650.0, # 1m size_x: float = 1450.0, # 1m
size_y: float = 1550.0, # 1m size_y: float = 1450.0, # 1m
size_z: float = 2100.0, # 0.9m size_z: float = 100.0, # 0.9m
origin: Coordinate = Coordinate(-4000, 2000, 0), origin: Coordinate = Coordinate(-2200, 0, 0),
category: str = "coin_cell_deck", category: str = "coin_cell_deck",
setup: bool = False, # 是否自动执行 setup setup: bool = False, # 是否自动执行 setup
): ):
@@ -1006,11 +560,10 @@ class CoincellDeck(Deck):
""" """
super().__init__( super().__init__(
name=name, name=name,
size_x=size_x, size_x=1450.0,
size_y=size_y, size_y=1450.0,
size_z=size_z, size_z=100.0,
origin=origin, origin=origin,
category=category,
) )
if setup: if setup:
self.setup() self.setup()
@@ -1018,146 +571,67 @@ class CoincellDeck(Deck):
def setup(self) -> None: def setup(self) -> None:
"""设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置""" """设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置"""
# ====================================== 子弹夹 ============================================ # ====================================== 子弹夹 ============================================
zip_dan_jia = ClipMagazine_four("zi_dan_jia", 80, 80, 10)
self.assign_child_resource(zip_dan_jia, Coordinate(x=1400, y=50, z=0))
zip_dan_jia2 = ClipMagazine_four("zi_dan_jia2", 80, 80, 10)
self.assign_child_resource(zip_dan_jia2, Coordinate(x=1600, y=200, z=0))
zip_dan_jia3 = ClipMagazine("zi_dan_jia3", 80, 80, 10)
self.assign_child_resource(zip_dan_jia3, Coordinate(x=1500, y=200, z=0))
zip_dan_jia4 = ClipMagazine("zi_dan_jia4", 80, 80, 10)
self.assign_child_resource(zip_dan_jia4, Coordinate(x=1500, y=300, z=0))
zip_dan_jia5 = ClipMagazine("zi_dan_jia5", 80, 80, 10)
self.assign_child_resource(zip_dan_jia5, Coordinate(x=1600, y=300, z=0))
zip_dan_jia6 = ClipMagazine("zi_dan_jia6", 80, 80, 10)
self.assign_child_resource(zip_dan_jia6, Coordinate(x=1530, y=500, z=0))
zip_dan_jia7 = ClipMagazine("zi_dan_jia7", 80, 80, 10)
self.assign_child_resource(zip_dan_jia7, Coordinate(x=1180, y=400, z=0))
zip_dan_jia8 = ClipMagazine("zi_dan_jia8", 80, 80, 10)
self.assign_child_resource(zip_dan_jia8, Coordinate(x=1280, y=400, z=0))
# 为子弹夹添加极片 # 正极片4个洞位2x2布局
for i in range(4): zhengji_zip = MagazineHolder_4_Cathode("正极&铝箔弹夹")
jipian = ElectrodeSheet(name=f"zi_dan_jia_jipian_{i}", size_x=12, size_y=12, size_z=0.1) self.assign_child_resource(zhengji_zip, Coordinate(x=402.0, y=830.0, z=0))
zip_dan_jia2.children[i].assign_child_resource(jipian, location=None)
for i in range(4): # 正极壳、平垫片6个洞位2x2+2布局
jipian2 = ElectrodeSheet(name=f"zi_dan_jia2_jipian_{i}", size_x=12, size_y=12, size_z=0.1) zhengjike_zip = MagazineHolder_6_Cathode("正极壳&平垫片弹夹")
zip_dan_jia.children[i].assign_child_resource(jipian2, location=None) self.assign_child_resource(zhengjike_zip, Coordinate(x=566.0, y=272.0, z=0))
for i in range(6):
jipian3 = ElectrodeSheet(name=f"zi_dan_jia3_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # 负极壳、弹垫片6个洞位2x2+2布局
zip_dan_jia3.children[i].assign_child_resource(jipian3, location=None) fujike_zip = MagazineHolder_6_Anode("负极壳&弹垫片弹夹")
for i in range(6): self.assign_child_resource(fujike_zip, Coordinate(x=474.0, y=276.0, z=0))
jipian4 = ElectrodeSheet(name=f"zi_dan_jia4_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia4.children[i].assign_child_resource(jipian4, location=None) # 成品弹夹6个洞位3x2布局
for i in range(6): chengpindanjia_zip = MagazineHolder_6_Battery("成品弹夹")
jipian5 = ElectrodeSheet(name=f"zi_dan_jia5_jipian_{i}", size_x=12, size_y=12, size_z=0.1) self.assign_child_resource(chengpindanjia_zip, Coordinate(x=260.0, y=156.0, z=0))
zip_dan_jia5.children[i].assign_child_resource(jipian5, location=None)
for i in range(6):
jipian6 = ElectrodeSheet(name=f"zi_dan_jia6_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia6.children[i].assign_child_resource(jipian6, location=None)
for i in range(6):
jipian7 = ElectrodeSheet(name=f"zi_dan_jia7_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia7.children[i].assign_child_resource(jipian7, location=None)
for i in range(6):
jipian8 = ElectrodeSheet(name=f"zi_dan_jia8_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia8.children[i].assign_child_resource(jipian8, location=None)
# ====================================== 物料板 ============================================ # ====================================== 物料板 ============================================
# 创建6个4*4的物料板 # 创建物料板料盘carrier- 4x4布局
liaopan1 = MaterialPlate(name="liaopan1", size_x=120, size_y=100, size_z=10.0, fill=True) # 负极料盘
self.assign_child_resource(liaopan1, Coordinate(x=1010, y=50, z=0)) fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
for i in range(16): self.assign_child_resource(fujiliaopan, Coordinate(x=708.0, y=794.0, z=0))
jipian_1 = ElectrodeSheet(name=f"{liaopan1.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # for i in range(16):
liaopan1.children[i].assign_child_resource(jipian_1, location=None) # 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)
liaopan2 = MaterialPlate(name="liaopan2", size_x=120, size_y=100, size_z=10.0, fill=True) # 隔膜料盘
self.assign_child_resource(liaopan2, Coordinate(x=1130, y=50, z=0)) 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)
liaopan3 = MaterialPlate(name="liaopan3", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(liaopan3, Coordinate(x=1250, y=50, z=0))
liaopan4 = MaterialPlate(name="liaopan4", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(liaopan4, Coordinate(x=1010, y=150, z=0))
for i in range(16):
jipian_4 = ElectrodeSheet(name=f"{liaopan4.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
liaopan4.children[i].assign_child_resource(jipian_4, location=None)
liaopan5 = MaterialPlate(name="liaopan5", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(liaopan5, Coordinate(x=1130, y=150, z=0))
liaopan6 = MaterialPlate(name="liaopan6", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(liaopan6, Coordinate(x=1250, y=150, z=0))
# ====================================== 瓶架、移液枪 ============================================ # ====================================== 瓶架、移液枪 ============================================
# 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒 # 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒
bottle_rack_3x4 = BottleRack( # 奔耀上料5ml分液瓶小板 - 由奔曜跨站转运而来,不单独写,但是这里应该有一个堆栈用于摆放分液瓶小板
name="bottle_rack_3x4",
size_x=210.0, # bottle_rack_3x4 = BottleRack(
size_y=140.0, # name="bottle_rack_3x4",
size_z=100.0, # size_x=210.0,
num_items_x=3, # size_y=140.0,
num_items_y=4, # size_z=100.0,
position_spacing=35.0, # num_items_x=2,
orientation="vertical", # num_items_y=4,
) # position_spacing=35.0,
self.assign_child_resource(bottle_rack_3x4, Coordinate(x=100, y=200, z=0)) # orientation="vertical",
# )
bottle_rack_6x2 = BottleRack( # self.assign_child_resource(bottle_rack_3x4, Coordinate(x=1542.0, y=717.0, z=0))
name="bottle_rack_6x2",
size_x=120.0, # 电解液缓存位 - 6x2布局
size_y=250.0, bottle_rack_6x2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2")
size_z=100.0, self.assign_child_resource(bottle_rack_6x2, Coordinate(x=1050.0, y=358.0, z=0))
num_items_x=6, # 电解液回收位6x2
num_items_y=2, bottle_rack_6x2_2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2_2")
position_spacing=35.0, self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=914.0, y=358.0, z=0))
orientation="vertical",
)
self.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0))
bottle_rack_6x2_2 = BottleRack(
name="bottle_rack_6x2_2",
size_x=120.0,
size_y=250.0,
size_z=100.0,
num_items_x=6,
num_items_y=2,
position_spacing=35.0,
orientation="vertical",
)
self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=430, y=300, z=0))
# 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位
for idx in range(bottle_rack_3x4.num_items_x * bottle_rack_3x4.num_items_y):
sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1)
bottle_rack_3x4.assign_child_resource(sheet, index=idx)
for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y):
sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1)
bottle_rack_6x2.assign_child_resource(sheet, index=idx)
tip_box = TipBox64(name="tip_box_64") tip_box = TipBox64(name="tip_box_64")
self.assign_child_resource(tip_box, Coordinate(x=300, y=100, z=0)) self.assign_child_resource(tip_box, Coordinate(x=782.0, y=514.0, z=0))
waste_tip_box = WasteTipBox(name="waste_tip_box") waste_tip_box = WasteTipBox(name="waste_tip_box")
self.assign_child_resource(waste_tip_box, Coordinate(x=300, y=200, z=0)) self.assign_child_resource(waste_tip_box, Coordinate(x=778.0, y=622.0, z=0))
print(self)
def create_coin_cell_deck(name: str = "coin_cell_deck", size_x: float = 1000.0, size_y: float = 1000.0, size_z: float = 900.0) -> CoincellDeck:
"""创建并配置标准的纽扣电池组装工作站台面
Args:
name: 台面名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
Returns:
已配置好的 CoincellDeck 对象
"""
# 创建 CoincellDeck 实例并自动执行 setup 配置
deck = CoincellDeck(name=name, size_x=size_x, size_y=size_y, size_z=size_z, setup=True)
return deck
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,4 +1,3 @@
import csv import csv
import inspect import inspect
import json import json
@@ -109,44 +108,22 @@ def _coerce_deck_input(deck: Any) -> Optional[Deck]:
#构建物料系统 #构建物料系统
class CoinCellAssemblyWorkstation(WorkstationBase): class CoinCellAssemblyWorkstation(WorkstationBase):
def __init__( def __init__(self,
self, config: dict = None,
deck: Deck=None, deck=None,
address: str = "172.16.28.102", address: str = "172.16.28.102",
port: str = "502", port: str = "502",
debug_mode: bool = False, debug_mode: bool = False,
*args, *args,
**kwargs, **kwargs):
):
if deck is None and "deck" in kwargs:
deck = kwargs.pop("deck")
else:
kwargs.pop("deck", None)
normalized_deck = _coerce_deck_input(deck) if deck is None and config:
deck = config.get('deck')
if deck is None and isinstance(normalized_deck, Deck): if deck is None:
deck = normalized_deck logger.info("没有传入依华deck检查启动json文件")
super().__init__(deck=deck, *args, **kwargs,)
super().__init__(
#桌子
deck=deck,
*args,
**kwargs,
)
self.debug_mode = debug_mode self.debug_mode = debug_mode
# 如果没有传入 deck则创建标准配置的 deck
if self.deck is None:
self.deck = CoincellDeck(size_x=1000, size_y=1000, size_z=900, origin=Coordinate(-800, 0, 0),setup=True)
else:
# 如果传入了 deck 但还没有 setup可以选择是否 setup
if self.deck is not None and len(self.deck.children) == 0:
# deck 为空,执行 setup
self.deck.setup()
# 否则使用传入的 deck可能已经配置好了
self.deck = self.deck
""" 连接初始化 """ """ 连接初始化 """
modbus_client = TCPClient(addr=address, port=port) modbus_client = TCPClient(addr=address, port=port)
logger.debug(f"创建 Modbus 客户端: {modbus_client}") logger.debug(f"创建 Modbus 客户端: {modbus_client}")
@@ -161,26 +138,20 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
time.sleep(2) time.sleep(2)
if not modbus_client.client.is_socket_open(): if not modbus_client.client.is_socket_open():
raise ValueError('modbus tcp connection failed') raise ValueError('modbus tcp connection failed')
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_1105.csv'))
self.client = modbus_client.register_node_list(self.nodes)
else: else:
print("测试模式,跳过连接") print("测试模式,跳过连接")
self.nodes, self.client = None, None
""" 工站的配置 """ """ 工站的配置 """
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_1105.csv'))
self.client = modbus_client.register_node_list(self.nodes)
self.success = False self.success = False
self.allow_data_read = False #允许读取函数运行标志位 self.allow_data_read = False #允许读取函数运行标志位
self.csv_export_thread = None self.csv_export_thread = None
self.csv_export_running = False self.csv_export_running = False
self.csv_export_file = None self.csv_export_file = None
self.coin_num_N = 0 #已组装电池数量 self.coin_num_N = 0 #已组装电池数量
#创建一个物料台面,包含两个极片板
#self._ros_node.update_resource(self.deck)
#ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
# "resources": [self.deck]
#})
def post_init(self, ros_node: ROS2WorkstationNode): def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node self._ros_node = ros_node
#self.deck = create_a_coin_cell_deck() #self.deck = create_a_coin_cell_deck()
@@ -188,6 +159,27 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"resources": [self.deck] "resources": [self.deck]
}) })
def sync_transfer_resources(self) -> Dict[str, Any]:
"""
供跨工站转运完成后调用,强制将当前台面资源同步到云端/前端。
"""
if not hasattr(self, "_ros_node") or self._ros_node is None:
return {"status": "failed", "error": "ros_node_not_ready"}
if self.deck is None:
return {"status": "failed", "error": "deck_not_initialized"}
try:
future = ROS2DeviceNode.run_async_func(
self._ros_node.update_resource,
True,
resources=[self.deck],
)
if future:
future.result()
return {"status": "success"}
except Exception as exc:
logger.error(f"同步转运资源失败: {exc}", exc_info=True)
return {"status": "failed", "error": str(exc)}
# 批量操作在这里写 # 批量操作在这里写
async def change_hole_sheet_to_2(self, hole: MaterialHole): async def change_hole_sheet_to_2(self, hole: MaterialHole):
hole._unilabos_state["max_sheets"] = 2 hole._unilabos_state["max_sheets"] = 2
@@ -818,7 +810,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
logger.debug(f"data_electrolyte_code: {data_electrolyte_code}") logger.debug(f"data_electrolyte_code: {data_electrolyte_code}")
logger.debug(f"data_coin_cell_code: {data_coin_cell_code}") logger.debug(f"data_coin_cell_code: {data_coin_cell_code}")
#接收完信息后读取完毕标志位置True #接收完信息后读取完毕标志位置True
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8") liaopan3 = self.deck.get_resource("成品弹夹")
#把物料解绑后放到另一盘上 #把物料解绑后放到另一盘上
battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2) battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2)
battery._unilabos_state = { battery._unilabos_state = {
@@ -1013,6 +1005,31 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
#self.success = True #self.success = True
#return self.success #return self.success
def run_packaging_workflow(self, workflow_config: Dict[str, Any]) -> "CoinCellAssemblyWorkstation":
config = workflow_config or {}
qiming_params = config.get("qiming") or {}
if qiming_params:
self.qiming_coin_cell_code(**qiming_params)
if config.get("init", True):
self.func_pack_device_init()
if config.get("auto", True):
self.func_pack_device_auto()
if config.get("start", True):
self.func_pack_device_start()
packaging_config = config.get("packaging") or {}
bottle_num = packaging_config.get("bottle_num")
if bottle_num is not None:
self.func_pack_send_bottle_num(bottle_num)
allpack_params = packaging_config.get("command") or {}
if allpack_params:
self.func_allpack_cmd(**allpack_params)
return self
def fun_wuliao_test(self) -> bool: def fun_wuliao_test(self) -> bool:
#找到data_init中构建的2个物料盘 #找到data_init中构建的2个物料盘
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8") liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
@@ -1035,7 +1052,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# time.sleep(1) # time.sleep(1)
# time.sleep(40) # 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循环 # 检查CSV导出是否正在运行已运行则跳出防止同时启动两个while循环
if self.csv_export_running: if self.csv_export_running:
return False, "读取已在运行中" return False, "读取已在运行中"
@@ -1226,14 +1243,95 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"""移液枪头库存 (数量, INT16)""" """移液枪头库存 (数量, INT16)"""
inventory, read_err = self.client.register_node_list(self.nodes).use_node('REG_DATA_TIPS_INVENTORY').read(1) inventory, read_err = self.client.register_node_list(self.nodes).use_node('REG_DATA_TIPS_INVENTORY').read(1)
return inventory return inventory
''' '''
def run_coin_cell_assembly_workflow(
self,
workflow_config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
config: Dict[str, Any]
if workflow_config is None:
config = {}
elif isinstance(workflow_config, list):
config = {"materials": workflow_config}
else:
config = workflow_config
qiming_defaults = {
"fujipian_panshu": 1,
"fujipian_juzhendianwei": 0,
"gemopanshu": 1,
"gemo_juzhendianwei": 0,
"lvbodian": True,
"battery_pressure_mode": True,
"battery_pressure": 4200,
"battery_clean_ignore": False,
}
qiming_params = {**qiming_defaults, **(config.get("qiming") or {})}
qiming_success = self.qiming_coin_cell_code(**qiming_params)
step_results: Dict[str, Any] = {}
try:
self.func_pack_device_init()
step_results["init"] = True
except Exception as exc:
step_results["init"] = f"error: {exc}"
try:
self.func_pack_device_auto()
step_results["auto"] = True
except Exception as exc:
step_results["auto"] = f"error: {exc}"
try:
self.func_pack_device_start()
step_results["start"] = True
except Exception as exc:
step_results["start"] = f"error: {exc}"
packaging_cfg = config.get("packaging") or {}
bottle_num = packaging_cfg.get("bottle_num", 1)
try:
self.func_pack_send_bottle_num(bottle_num)
step_results["send_bottle_num"] = True
except Exception as exc:
step_results["send_bottle_num"] = f"error: {exc}"
command_defaults = {
"elec_num": 1,
"elec_use_num": 1,
"elec_vol": 50,
"assembly_type": 7,
"assembly_pressure": 4200,
"file_path": "/Users/sml/work",
}
command_params = {**command_defaults, **(packaging_cfg.get("command") or {})}
packaging_result = self.func_allpack_cmd(**command_params)
finished_result = self.func_pack_send_finished_cmd()
stop_result = self.func_pack_device_stop()
return {
"qiming": {
"params": qiming_params,
"success": qiming_success,
},
"workflow_steps": step_results,
"packaging": {
"bottle_num": bottle_num,
"command": command_params,
"result": packaging_result,
},
"finish": {
"send_finished": finished_result,
"stop": stop_result,
},
}
if __name__ == "__main__": if __name__ == "__main__":
# 简单测试 deck = CoincellDeck(setup=True, name="coin_cell_deck")
workstation = CoinCellAssemblyWorkstation() w = CoinCellAssemblyWorkstation(deck=deck, address="172.16.28.102", port="502", debug_mode=False)
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) w.run_coin_cell_assembly_workflow()
print(f"工作站创建成功: {workstation.deck.name}")
print(f"料盘数量: {len(workstation.deck.children)}")

View File

@@ -1,583 +0,0 @@
"""
工作站物料管理基类
Workstation Material Management Base Class
基于PyLabRobot的物料管理系统
"""
from typing import Dict, Any, List, Optional, Union, Type
from abc import ABC, abstractmethod
import json
from pylabrobot.resources import (
Resource as PLRResource,
Container,
Deck,
Coordinate as PLRCoordinate,
)
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
from unilabos.utils.log import logger
from unilabos.resources.graphio import resource_plr_to_ulab, resource_ulab_to_plr
class MaterialManagementBase(ABC):
"""物料管理基类
定义工作站物料管理的标准接口:
1. 物料初始化 - 根据配置创建物料资源
2. 物料追踪 - 实时跟踪物料位置和状态
3. 物料查找 - 按类型、位置、状态查找物料
4. 物料转换 - PyLabRobot与UniLab资源格式转换
"""
def __init__(
self,
device_id: str,
deck_config: Dict[str, Any],
resource_tracker: DeviceNodeResourceTracker,
children_config: Dict[str, Dict[str, Any]] = None
):
self.device_id = device_id
self.deck_config = deck_config
self.resource_tracker = resource_tracker
self.children_config = children_config or {}
# 创建主台面
self.plr_deck = self._create_deck()
# 扩展ResourceTracker
self._extend_resource_tracker()
# 注册deck到resource tracker
self.resource_tracker.add_resource(self.plr_deck)
# 初始化子资源
self.plr_resources = {}
self._initialize_materials()
def _create_deck(self) -> Deck:
"""创建主台面"""
return Deck(
name=f"{self.device_id}_deck",
size_x=self.deck_config.get("size_x", 1000.0),
size_y=self.deck_config.get("size_y", 1000.0),
size_z=self.deck_config.get("size_z", 500.0),
origin=PLRCoordinate(0, 0, 0)
)
def _extend_resource_tracker(self):
"""扩展ResourceTracker以支持PyLabRobot特定功能"""
def find_by_type(resource_type):
"""按类型查找资源"""
return self._find_resources_by_type_recursive(self.plr_deck, resource_type)
def find_by_category(category: str):
"""按类别查找资源"""
found = []
for resource in self._get_all_resources():
if hasattr(resource, 'category') and resource.category == category:
found.append(resource)
return found
def find_by_name_pattern(pattern: str):
"""按名称模式查找资源"""
import re
found = []
for resource in self._get_all_resources():
if re.search(pattern, resource.name):
found.append(resource)
return found
# 动态添加方法到resource_tracker
self.resource_tracker.find_by_type = find_by_type
self.resource_tracker.find_by_category = find_by_category
self.resource_tracker.find_by_name_pattern = find_by_name_pattern
def _find_resources_by_type_recursive(self, resource, target_type):
"""递归查找指定类型的资源"""
found = []
if isinstance(resource, target_type):
found.append(resource)
# 递归查找子资源
children = getattr(resource, "children", [])
for child in children:
found.extend(self._find_resources_by_type_recursive(child, target_type))
return found
def _get_all_resources(self) -> List[PLRResource]:
"""获取所有资源"""
all_resources = []
def collect_resources(resource):
all_resources.append(resource)
children = getattr(resource, "children", [])
for child in children:
collect_resources(child)
collect_resources(self.plr_deck)
return all_resources
def _initialize_materials(self):
"""初始化物料"""
try:
# 确定创建顺序,确保父资源先于子资源创建
creation_order = self._determine_creation_order()
# 按顺序创建资源
for resource_id in creation_order:
config = self.children_config[resource_id]
self._create_plr_resource(resource_id, config)
logger.info(f"物料管理系统初始化完成,共创建 {len(self.plr_resources)} 个资源")
except Exception as e:
logger.error(f"物料初始化失败: {e}")
def _determine_creation_order(self) -> List[str]:
"""确定资源创建顺序"""
order = []
visited = set()
def visit(resource_id: str):
if resource_id in visited:
return
visited.add(resource_id)
config = self.children_config.get(resource_id, {})
parent_id = config.get("parent")
# 如果有父资源,先访问父资源
if parent_id and parent_id in self.children_config:
visit(parent_id)
order.append(resource_id)
for resource_id in self.children_config:
visit(resource_id)
return order
def _create_plr_resource(self, resource_id: str, config: Dict[str, Any]):
"""创建PyLabRobot资源"""
try:
resource_type = config.get("type", "unknown")
data = config.get("data", {})
location_config = config.get("location", {})
# 创建位置坐标
location = PLRCoordinate(
x=location_config.get("x", 0.0),
y=location_config.get("y", 0.0),
z=location_config.get("z", 0.0)
)
# 根据类型创建资源
resource = self._create_resource_by_type(resource_id, resource_type, config, data, location)
if resource:
# 设置父子关系
parent_id = config.get("parent")
if parent_id and parent_id in self.plr_resources:
parent_resource = self.plr_resources[parent_id]
parent_resource.assign_child_resource(resource, location)
else:
# 直接放在deck上
self.plr_deck.assign_child_resource(resource, location)
# 保存资源引用
self.plr_resources[resource_id] = resource
# 注册到resource tracker
self.resource_tracker.add_resource(resource)
logger.debug(f"创建资源成功: {resource_id} ({resource_type})")
except Exception as e:
logger.error(f"创建资源失败 {resource_id}: {e}")
@abstractmethod
def _create_resource_by_type(
self,
resource_id: str,
resource_type: str,
config: Dict[str, Any],
data: Dict[str, Any],
location: PLRCoordinate
) -> Optional[PLRResource]:
"""根据类型创建资源 - 子类必须实现"""
pass
# ============ 物料查找接口 ============
def find_materials_by_type(self, material_type: str) -> List[PLRResource]:
"""按材料类型查找物料"""
return self.resource_tracker.find_by_category(material_type)
def find_material_by_id(self, resource_id: str) -> Optional[PLRResource]:
"""按ID查找物料"""
return self.plr_resources.get(resource_id)
def find_available_positions(self, position_type: str) -> List[PLRResource]:
"""查找可用位置"""
positions = self.resource_tracker.find_by_category(position_type)
available = []
for pos in positions:
if hasattr(pos, 'is_available') and pos.is_available():
available.append(pos)
elif hasattr(pos, 'children') and len(pos.children) == 0:
available.append(pos)
return available
def get_material_inventory(self) -> Dict[str, int]:
"""获取物料库存统计"""
inventory = {}
for resource in self._get_all_resources():
if hasattr(resource, 'category'):
category = resource.category
inventory[category] = inventory.get(category, 0) + 1
return inventory
# ============ 物料状态更新接口 ============
def update_material_location(self, material_id: str, new_location: PLRCoordinate) -> bool:
"""更新物料位置"""
try:
material = self.find_material_by_id(material_id)
if material:
material.location = new_location
return True
return False
except Exception as e:
logger.error(f"更新物料位置失败: {e}")
return False
def move_material(self, material_id: str, target_container_id: str) -> bool:
"""移动物料到目标容器"""
try:
material = self.find_material_by_id(material_id)
target = self.find_material_by_id(target_container_id)
if material and target:
# 从原位置移除
if material.parent:
material.parent.unassign_child_resource(material)
# 添加到新位置
target.assign_child_resource(material)
return True
return False
except Exception as e:
logger.error(f"移动物料失败: {e}")
return False
# ============ 资源转换接口 ============
def convert_to_unilab_format(self, plr_resource: PLRResource) -> Dict[str, Any]:
"""将PyLabRobot资源转换为UniLab格式"""
return resource_plr_to_ulab(plr_resource)
def convert_from_unilab_format(self, unilab_resource: Dict[str, Any]) -> PLRResource:
"""将UniLab格式转换为PyLabRobot资源"""
return resource_ulab_to_plr(unilab_resource)
def get_deck_state(self) -> Dict[str, Any]:
"""获取Deck状态"""
try:
return {
"deck_info": {
"name": self.plr_deck.name,
"size": {
"x": self.plr_deck.size_x,
"y": self.plr_deck.size_y,
"z": self.plr_deck.size_z
},
"children_count": len(self.plr_deck.children)
},
"resources": {
resource_id: self.convert_to_unilab_format(resource)
for resource_id, resource in self.plr_resources.items()
},
"inventory": self.get_material_inventory()
}
except Exception as e:
logger.error(f"获取Deck状态失败: {e}")
return {"error": str(e)}
# ============ 数据持久化接口 ============
def save_state_to_file(self, file_path: str) -> bool:
"""保存状态到文件"""
try:
state = self.get_deck_state()
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(state, f, indent=2, ensure_ascii=False)
logger.info(f"状态已保存到: {file_path}")
return True
except Exception as e:
logger.error(f"保存状态失败: {e}")
return False
def load_state_from_file(self, file_path: str) -> bool:
"""从文件加载状态"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
state = json.load(f)
# 重新创建资源
self._recreate_resources_from_state(state)
logger.info(f"状态已从文件加载: {file_path}")
return True
except Exception as e:
logger.error(f"加载状态失败: {e}")
return False
def _recreate_resources_from_state(self, state: Dict[str, Any]):
"""从状态重新创建资源"""
# 清除现有资源
self.plr_resources.clear()
self.plr_deck.children.clear()
# 从状态重新创建
resources_data = state.get("resources", {})
for resource_id, resource_data in resources_data.items():
try:
plr_resource = self.convert_from_unilab_format(resource_data)
self.plr_resources[resource_id] = plr_resource
self.plr_deck.assign_child_resource(plr_resource)
except Exception as e:
logger.error(f"重新创建资源失败 {resource_id}: {e}")
class CoinCellMaterialManagement(MaterialManagementBase):
"""纽扣电池物料管理类
从 button_battery_station 抽取的物料管理功能
"""
def _create_resource_by_type(
self,
resource_id: str,
resource_type: str,
config: Dict[str, Any],
data: Dict[str, Any],
location: PLRCoordinate
) -> Optional[PLRResource]:
"""根据类型创建纽扣电池相关资源"""
# 导入纽扣电池资源类
from unilabos.device_comms.button_battery_station import (
MaterialPlate, PlateSlot, ClipMagazine, BatteryPressSlot,
TipBox64, WasteTipBox, BottleRack, Battery, ElectrodeSheet
)
try:
if resource_type == "material_plate":
return self._create_material_plate(resource_id, config, data, location)
elif resource_type == "plate_slot":
return self._create_plate_slot(resource_id, config, data, location)
elif resource_type == "clip_magazine":
return self._create_clip_magazine(resource_id, config, data, location)
elif resource_type == "battery_press_slot":
return self._create_battery_press_slot(resource_id, config, data, location)
elif resource_type == "tip_box":
return self._create_tip_box(resource_id, config, data, location)
elif resource_type == "waste_tip_box":
return self._create_waste_tip_box(resource_id, config, data, location)
elif resource_type == "bottle_rack":
return self._create_bottle_rack(resource_id, config, data, location)
elif resource_type == "battery":
return self._create_battery(resource_id, config, data, location)
else:
logger.warning(f"未知的资源类型: {resource_type}")
return None
except Exception as e:
logger.error(f"创建资源失败 {resource_id} ({resource_type}): {e}")
return None
def _create_material_plate(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
"""创建料板"""
from unilabos.device_comms.button_battery_station import MaterialPlate, ElectrodeSheet
plate = MaterialPlate(
name=resource_id,
size_x=config.get("size_x", 80.0),
size_y=config.get("size_y", 80.0),
size_z=config.get("size_z", 10.0),
hole_diameter=config.get("hole_diameter", 15.0),
hole_depth=config.get("hole_depth", 8.0),
hole_spacing_x=config.get("hole_spacing_x", 20.0),
hole_spacing_y=config.get("hole_spacing_y", 20.0),
number=data.get("number", "")
)
plate.location = location
# 如果有预填充的极片数据,创建极片
electrode_sheets = data.get("electrode_sheets", [])
for i, sheet_data in enumerate(electrode_sheets):
if i < len(plate.children): # 确保不超过洞位数量
hole = plate.children[i]
sheet = ElectrodeSheet(
name=f"{resource_id}_sheet_{i}",
diameter=sheet_data.get("diameter", 14.0),
thickness=sheet_data.get("thickness", 0.1),
mass=sheet_data.get("mass", 0.01),
material_type=sheet_data.get("material_type", "cathode"),
info=sheet_data.get("info", "")
)
hole.place_electrode_sheet(sheet)
return plate
def _create_plate_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
"""创建板槽位"""
from unilabos.device_comms.button_battery_station import PlateSlot
slot = PlateSlot(
name=resource_id,
max_plates=config.get("max_plates", 8)
)
slot.location = location
return slot
def _create_clip_magazine(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
"""创建子弹夹"""
from unilabos.device_comms.button_battery_station import ClipMagazine
magazine = ClipMagazine(
name=resource_id,
size_x=config.get("size_x", 150.0),
size_y=config.get("size_y", 100.0),
size_z=config.get("size_z", 50.0),
hole_diameter=config.get("hole_diameter", 15.0),
hole_depth=config.get("hole_depth", 40.0),
hole_spacing=config.get("hole_spacing", 25.0),
max_sheets_per_hole=config.get("max_sheets_per_hole", 100)
)
magazine.location = location
return magazine
def _create_battery_press_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
"""创建电池压制槽"""
from unilabos.device_comms.button_battery_station import BatteryPressSlot
slot = BatteryPressSlot(
name=resource_id,
diameter=config.get("diameter", 20.0),
depth=config.get("depth", 15.0)
)
slot.location = location
return slot
def _create_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
"""创建枪头盒"""
from unilabos.device_comms.button_battery_station import TipBox64
tip_box = TipBox64(
name=resource_id,
size_x=config.get("size_x", 127.8),
size_y=config.get("size_y", 85.5),
size_z=config.get("size_z", 60.0),
with_tips=data.get("with_tips", True)
)
tip_box.location = location
return tip_box
def _create_waste_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
"""创建废枪头盒"""
from unilabos.device_comms.button_battery_station import WasteTipBox
waste_box = WasteTipBox(
name=resource_id,
size_x=config.get("size_x", 127.8),
size_y=config.get("size_y", 85.5),
size_z=config.get("size_z", 60.0),
max_tips=config.get("max_tips", 100)
)
waste_box.location = location
return waste_box
def _create_bottle_rack(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
"""创建瓶架"""
from unilabos.device_comms.button_battery_station import BottleRack
rack = BottleRack(
name=resource_id,
size_x=config.get("size_x", 210.0),
size_y=config.get("size_y", 140.0),
size_z=config.get("size_z", 100.0),
bottle_diameter=config.get("bottle_diameter", 30.0),
bottle_height=config.get("bottle_height", 100.0),
position_spacing=config.get("position_spacing", 35.0)
)
rack.location = location
return rack
def _create_battery(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
"""创建电池"""
from unilabos.device_comms.button_battery_station import Battery
battery = Battery(
name=resource_id,
diameter=config.get("diameter", 20.0),
height=config.get("height", 3.2),
max_volume=config.get("max_volume", 100.0),
barcode=data.get("barcode", "")
)
battery.location = location
return battery
# ============ 纽扣电池特定查找方法 ============
def find_material_plates(self):
"""查找所有料板"""
from unilabos.device_comms.button_battery_station import MaterialPlate
return self.resource_tracker.find_by_type(MaterialPlate)
def find_batteries(self):
"""查找所有电池"""
from unilabos.device_comms.button_battery_station import Battery
return self.resource_tracker.find_by_type(Battery)
def find_electrode_sheets(self):
"""查找所有极片"""
found = []
plates = self.find_material_plates()
for plate in plates:
for hole in plate.children:
if hasattr(hole, 'has_electrode_sheet') and hole.has_electrode_sheet():
found.append(hole._electrode_sheet)
return found
def find_plate_slots(self):
"""查找所有板槽位"""
from unilabos.device_comms.button_battery_station import PlateSlot
return self.resource_tracker.find_by_type(PlateSlot)
def find_clip_magazines(self):
"""查找所有子弹夹"""
from unilabos.device_comms.button_battery_station import ClipMagazine
return self.resource_tracker.find_by_type(ClipMagazine)
def find_press_slots(self):
"""查找所有压制槽"""
from unilabos.device_comms.button_battery_station import BatteryPressSlot
return self.resource_tracker.find_by_type(BatteryPressSlot)

View File

@@ -4,7 +4,6 @@ bioyond_cell:
class: class:
action_value_mappings: action_value_mappings:
auto-auto_batch_outbound_from_xlsx: auto-auto_batch_outbound_from_xlsx:
display_name: 批量导入上料
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
@@ -138,7 +137,7 @@ bioyond_cell:
WH4_x5_y1_z1_5_quantity: 0.0 WH4_x5_y1_z1_5_quantity: 0.0
WH4_x5_y2_z1_10_materialName: '' WH4_x5_y2_z1_10_materialName: ''
WH4_x5_y2_z1_10_quantity: 0.0 WH4_x5_y2_z1_10_quantity: 0.0
xlsx_path: C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx xlsx_path: /Users/sml/work/Unilab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -464,7 +463,7 @@ bioyond_cell:
default: 0.0 default: 0.0
type: number type: number
xlsx_path: xlsx_path:
default: C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx default: /Users/sml/work/Unilab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx
type: string type: string
required: [] required: []
type: object type: object
@@ -600,6 +599,7 @@ bioyond_cell:
bottle_type: null bottle_type: null
location_code: null location_code: null
name: null name: null
warehouse_name: 手动堆栈
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -617,6 +617,9 @@ bioyond_cell:
type: string type: string
name: name:
type: string type: string
warehouse_name:
default: 手动堆栈
type: string
required: required:
- name - name
- board_type - board_type
@@ -785,6 +788,187 @@ bioyond_cell:
title: report_material_change参数 title: report_material_change参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-resource_tree_transfer:
feedback: {}
goal: {}
goal_default:
old_parent: null
parent_resource: null
plr_resource: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
old_parent:
type: object
parent_resource:
type: object
plr_resource:
type: object
required:
- old_parent
- plr_resource
- parent_resource
type: object
result: {}
required:
- goal
title: resource_tree_transfer参数
type: object
type: UniLabJsonCommand
auto-run_feeding_stage:
feedback: {}
goal: {}
goal_default: {}
handles:
input: []
output:
- data_key: feeding_materials
data_source: executor
data_type: resource
handler_key: feeding_materials
label: Feeding Materials
placeholder_keys: {}
result:
properties:
feeding_materials:
items:
type: object
type: array
required:
- feeding_materials
type: object
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: run_feeding_stage参数
type: object
type: UniLabJsonCommand
auto-run_liquid_preparation_stage:
feedback: {}
goal: {}
goal_default: {}
handles:
input:
- data_key: feeding_materials
data_source: handle
data_type: resource
handler_key: feeding_materials
label: Feeding Materials
output:
- data_key: liquid_materials
data_source: executor
data_type: resource
handler_key: liquid_materials
label: Liquid Materials
placeholder_keys: {}
result:
properties:
feeding_materials:
items:
type: object
type: array
liquid_materials:
items:
type: object
type: array
required:
- liquid_materials
type: object
schema:
description: ''
properties:
feedback: {}
goal:
properties:
feeding_materials:
items:
type: object
type: array
required: []
type: object
result: {}
required:
- goal
title: run_liquid_preparation_stage参数
type: object
type: UniLabJsonCommand
auto-run_transfer_stage:
feedback: {}
goal: {}
goal_default: {}
handles:
input:
- data_key: liquid_materials
data_source: handle
data_type: resource
handler_key: liquid_materials
label: Liquid Materials
output:
- data_key: transfer_materials
data_source: executor
data_type: resource
handler_key: transfer_materials
label: Transfer Materials
placeholder_keys: {}
result:
properties:
liquid_materials:
items:
type: object
type: array
transfer_materials:
items:
type: object
type: array
transfer_summary:
type: object
required:
- transfer_materials
type: object
schema:
description: ''
properties:
feedback: {}
goal:
properties:
liquid_materials:
items:
type: object
type: array
required: []
type: object
result:
properties:
liquid_materials:
items:
type: object
type: array
transfer_materials:
items:
type: object
type: array
transfer_summary:
type: object
type: object
required:
- goal
title: run_transfer_stage参数
type: object
type: UniLabJsonCommand
auto-scheduler_continue: auto-scheduler_continue:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -1072,12 +1256,13 @@ bioyond_cell:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation
status_types: {} status_types:
device_id: String
type: python type: python
config_info: [] config_info: []
description: '' description: 配液工站
handles: [] handles: []
icon: '' icon: benyao2.webp
init_param_schema: init_param_schema:
config: config:
properties: properties:
@@ -1090,8 +1275,11 @@ bioyond_cell:
required: [] required: []
type: object type: object
data: data:
properties: {} properties:
required: [] device_id:
type: string
required:
- device_id
type: object type: object
registry_type: device registry_type: device
version: 1.0.0 version: 1.0.0

View File

@@ -79,7 +79,7 @@ coincellassemblyworkstation_device:
elec_num: null elec_num: null
elec_use_num: null elec_use_num: null
elec_vol: 50 elec_vol: 50
file_path: C:\Users\67484\Desktop file_path: /Users/sml/work
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -103,7 +103,7 @@ coincellassemblyworkstation_device:
default: 50 default: 50
type: integer type: integer
file_path: file_path:
default: C:\Users\67484\Desktop default: /Users/sml/work
type: string type: string
required: required:
- elec_num - elec_num
@@ -332,7 +332,7 @@ coincellassemblyworkstation_device:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
file_path: D:\coin_cell_data file_path: /Users/sml/work
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -343,7 +343,7 @@ coincellassemblyworkstation_device:
goal: goal:
properties: properties:
file_path: file_path:
default: D:\coin_cell_data default: /Users/sml/work
type: string type: string
required: [] required: []
type: object type: object
@@ -477,6 +477,171 @@ coincellassemblyworkstation_device:
title: qiming_coin_cell_code参数 title: qiming_coin_cell_code参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-run_coin_cell_assembly_workflow:
feedback: {}
goal:
properties:
workflow_config:
type: object
required: []
type: object
goal_default:
workflow_config: {}
handles:
input:
- data_key: workflow_config
data_source: handle
data_type: resource
handler_key: WorkflowConfig
label: Workflow Config
output:
- data_key: qiming
data_source: executor
data_type: resource
handler_key: QimingResult
label: Qiming Result
- data_key: workflow_steps
data_source: executor
data_type: resource
handler_key: WorkflowSteps
label: Workflow Steps
- data_key: packaging
data_source: executor
data_type: resource
handler_key: PackagingResult
label: Packaging Result
- data_key: finish
data_source: executor
data_type: resource
handler_key: FinishResult
label: Finish Result
placeholder_keys: {}
result:
properties:
finish:
properties:
send_finished:
type: object
stop:
type: object
required:
- send_finished
- stop
type: object
packaging:
properties:
bottle_num:
type: integer
command:
type: object
result:
type: object
required:
- bottle_num
- command
- result
type: object
qiming:
properties:
params:
type: object
success:
type: boolean
required:
- params
- success
type: object
workflow_steps:
type: object
required:
- qiming
- workflow_steps
- packaging
- finish
type: object
schema:
description: ''
properties:
feedback: {}
goal:
properties:
workflow_config:
type: object
required: []
type: object
result:
properties:
finish:
properties:
send_finished:
type: object
stop:
type: object
required:
- send_finished
- stop
type: object
packaging:
properties:
bottle_num:
type: integer
command:
type: object
result:
type: object
required:
- bottle_num
- command
- result
type: object
qiming:
properties:
params:
type: object
success:
type: boolean
required:
- params
- success
type: object
workflow_steps:
type: object
required:
- qiming
- workflow_steps
- packaging
- finish
type: object
required:
- goal
title: run_coin_cell_assembly_workflow参数
type: object
type: UniLabJsonCommand
auto-run_packaging_workflow:
feedback: {}
goal: {}
goal_default:
workflow_config: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
workflow_config:
type: object
required:
- workflow_config
type: object
result: {}
required:
- goal
title: run_packaging_workflow参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
status_types: status_types:
data_assembly_coin_cell_num: int data_assembly_coin_cell_num: int
@@ -500,20 +665,22 @@ coincellassemblyworkstation_device:
sys_status: str sys_status: str
type: python type: python
config_info: [] config_info: []
description: '' description: 扣电工站
handles: [] handles: []
icon: coin_cell_assembly_picture.webp icon: koudian.webp
init_param_schema: init_param_schema:
config: config:
properties: properties:
address: address:
default: 172.21.32.111 default: 172.16.28.102
type: string type: string
config:
type: object
debug_mode: debug_mode:
default: false default: false
type: boolean type: boolean
deck: deck:
type: object type: string
port: port:
default: '502' default: '502'
type: string type: string

View File

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

View File

@@ -1,6 +1,8 @@
neware_battery_test_system: neware_battery_test_system:
category: category:
- neware_battery_test_system - neware_battery_test_system
- neware
- battery_test
class: class:
action_value_mappings: action_value_mappings:
auto-post_init: auto-post_init:
@@ -70,6 +72,38 @@ neware_battery_test_system:
title: test_connection参数 title: test_connection参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
debug_resource_names:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result:
return_info: return_info
success: success
schema:
description: 调试方法:显示所有资源的实际名称
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result:
properties:
return_info:
description: 资源调试信息
type: string
success:
description: 是否成功
type: boolean
required:
- return_info
- success
type: object
required:
- goal
type: object
type: UniLabJsonCommand
export_status_json: export_status_json:
feedback: {} feedback: {}
goal: goal:
@@ -219,7 +253,9 @@ neware_battery_test_system:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
@@ -252,6 +288,56 @@ neware_battery_test_system:
title: StrSingleInput title: StrSingleInput
type: object type: object
type: StrSingleInput type: StrSingleInput
submit_from_csv:
feedback: {}
goal:
csv_path: string
output_dir: string
goal_default:
csv_path: ''
output_dir: .
handles: {}
result:
return_info: return_info
submitted_count: submitted_count
success: success
schema:
description: 从CSV文件批量提交Neware测试任务
properties:
feedback: {}
goal:
properties:
csv_path:
description: 输入CSV文件的绝对路径
type: string
output_dir:
description: 输出目录用于存储XML和备份文件默认当前目录
type: string
required:
- csv_path
type: object
result:
properties:
return_info:
description: 执行结果详细信息
type: string
submitted_count:
description: 成功提交的任务数量
type: integer
success:
description: 是否成功
type: boolean
total_count:
description: CSV文件中的总行数
type: integer
required:
- return_info
- success
type: object
required:
- goal
type: object
type: UniLabJsonCommand
test_connection_action: test_connection_action:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -284,7 +370,7 @@ neware_battery_test_system:
- goal - goal
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.battery.neware_battery_test_system:NewareBatteryTestSystem module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
status_types: status_types:
channel_status: dict channel_status: dict
connection_info: dict connection_info: dict
@@ -294,7 +380,7 @@ neware_battery_test_system:
total_channels: int total_channels: int
type: python type: python
config_info: [] config_info: []
description: 新威电池测试系统驱动,支持720个通道的电池测试状态监控和数据导出。通过TCP通信实现远程控制包含完整的物料管理系统,支持2盘电池状态映射和监控 description: 新威电池测试系统驱动,提供720个通道的电池测试状态监控、物料管理和CSV批量提交功能。支持TCP通信实现远程控制包含完整的物料管理系统2盘电池状态映射以及从CSV文件批量提交测试任务的能力
handles: [] handles: []
icon: '' icon: ''
init_param_schema: init_param_schema:
@@ -310,13 +396,13 @@ neware_battery_test_system:
port: port:
type: integer type: integer
size_x: size_x:
default: 500.0 default: 50
type: number type: number
size_y: size_y:
default: 500.0 default: 50
type: number type: number
size_z: size_z:
default: 2000.0 default: 20
type: number type: number
timeout: timeout:
type: integer type: integer

View File

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

View File

@@ -1,16 +1,3 @@
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_20ml_fenyeping: YB_20ml_fenyeping:
category: category:
- yb3 - yb3
@@ -37,6 +24,19 @@ YB_5ml_fenyeping:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 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: YB_pei_ye_da_Bottle:
category: category:
- yb3 - yb3
@@ -63,3 +63,30 @@ YB_pei_ye_xiao_Bottle:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 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

@@ -11,71 +11,6 @@ YB_100ml_yeti:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
YB_1BottleCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_1BottleCarrier
type: pylabrobot
description: YB_1BottleCarrier
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_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_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_20ml_fenyepingban: YB_20ml_fenyepingban:
category: category:
- yb3 - yb3
@@ -102,40 +37,27 @@ YB_5ml_fenyepingban:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
YB_peiyepingxiaoban: YB_6StockCarrier:
category: category:
- yb3 - yb3
- YB_bottle_carriers - YB_bottle_carriers
class: class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier
type: pylabrobot type: pylabrobot
description: YB_peiyepingxiaoban description: YB_6StockCarrier
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
YB_shi_pei_qi_kuai: YB_6VialCarrier:
category: category:
- yb3 - yb3
- YB_bottle_carriers - YB_bottle_carriers
class: class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier
type: pylabrobot type: pylabrobot
description: YB_shi_pei_qi_kuai description: YB_6VialCarrier
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: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
@@ -154,14 +76,14 @@ YB_gao_nian_ye_Bottle:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
YB_jia_yang_tou_da: YB_gaonianye:
category: category:
- yb3 - yb3
- YB_bottle_carriers - YB_bottle_carriers
class: class:
module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye
type: pylabrobot type: pylabrobot
description: YB_jia_yang_tou_da description: YB_gaonianye
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
@@ -180,6 +102,71 @@ YB_jia_yang_tou_da_Carrier:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 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: YB_ye_100ml_Bottle:
category: category:
- yb3 - yb3
@@ -193,16 +180,3 @@ YB_ye_100ml_Bottle:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
YB_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
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

@@ -34,3 +34,15 @@ BIOYOND_YB_Deck:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 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

@@ -206,7 +206,7 @@ def YB_6VialCarrier(name: str) -> BottleCarrier:
return carrier return carrier
# 1瓶载架 - 单个中央位置 # 1瓶载架 - 单个中央位置
def YB_1BottleCarrier(name: str) -> BottleCarrier: def YB_ye(name: str) -> BottleCarrier:
# 载架尺寸 (mm) # 载架尺寸 (mm)
carrier_size_x = 127.8 carrier_size_x = 127.8
@@ -233,7 +233,7 @@ def YB_1BottleCarrier(name: str) -> BottleCarrier:
resource_size_y=beaker_diameter, resource_size_y=beaker_diameter,
name_prefix=name, name_prefix=name,
), ),
model="YB_1BottleCarrier", model="YB_ye",
) )
carrier.num_items_x = 1 carrier.num_items_x = 1
carrier.num_items_y = 1 carrier.num_items_y = 1

View File

@@ -33,7 +33,7 @@ def YB_ye_Bottle(
height=height, height=height,
max_volume=max_volume, max_volume=max_volume,
barcode=barcode, barcode=barcode,
model="Liquid_Bottle", model="YB_ye_Bottle",
) )
"""100ml液体""" """100ml液体"""

View File

@@ -89,23 +89,19 @@ class BIOYOND_YB_Deck(Deck):
"自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"), "自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"),
"手动堆栈-左": bioyond_warehouse_3x5x1("手动堆栈-左"), "手动堆栈-左": bioyond_warehouse_3x5x1("手动堆栈-左"),
"手动堆栈-右": bioyond_warehouse_3x5x1("手动堆栈-右"), "手动堆栈-右": bioyond_warehouse_3x5x1("手动堆栈-右"),
"粉末加样头堆栈-左": bioyond_warehouse_10x1x1("粉末加样头堆栈-左"), "粉末加样头堆栈": bioyond_warehouse_20x1x1("粉末加样头堆栈"),
"粉末加样头堆栈-右": bioyond_warehouse_10x1x1("粉末加样头堆栈-右"),
"配液站内试剂仓库": bioyond_warehouse_3x3x1("配液站内试剂仓库"), "配液站内试剂仓库": bioyond_warehouse_3x3x1("配液站内试剂仓库"),
"试剂替换仓库-左": bioyond_warehouse_5x1x1("试剂替换仓库-左"), "试剂替换仓库": bioyond_warehouse_10x1x1("试剂替换仓库"),
"试剂替换仓库-右": bioyond_warehouse_5x1x1("试剂替换仓库-右"),
} }
# warehouse 的位置 # warehouse 的位置
self.warehouse_locations = { self.warehouse_locations = {
"自动堆栈-左": Coordinate(-300.0, 158.0, 0.0), "自动堆栈-左": Coordinate(-100.3, 171.5, 0.0),
"自动堆栈-右": Coordinate(4160.0, 158.0, 0.0), "自动堆栈-右": Coordinate(3960.1, 155.9, 0.0),
"手动堆栈-左": Coordinate(-400.0, 877.0, 0.0), "手动堆栈-左": Coordinate(-213.3, 804.4, 0.0),
"手动堆栈-右": Coordinate(4160.0, 877.0, 0.0), "手动堆栈-右": Coordinate(3960.1, 807.6, 0.0),
"粉末加样头堆栈-左": Coordinate(415.0, 1301.0, 0.0), "粉末加样头堆栈": Coordinate(415.0, 1301.0, 0.0),
"粉末加样头堆栈-右": Coordinate(2200.0, 1304.0, 0.0), "配液站内试剂仓库": Coordinate(2162.0, 437.0, 0.0),
"配液站内试剂仓库": Coordinate(2162.0, 337.0, 0.0), "试剂替换仓库": Coordinate(1173.0, 802.0, 0.0),
"试剂替换仓库-左": Coordinate(1173.0, 702.0, 0.0),
"试剂替换仓库-右": Coordinate(2721.0, 739.0, 0.0),
} }
for warehouse_name, warehouse in self.warehouses.items(): for warehouse_name, warehouse in self.warehouses.items():

View File

@@ -674,10 +674,15 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
for loc in material.get("locations", []): for loc in material.get("locations", []):
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses: if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
warehouse = deck.warehouses[loc["whName"]] warehouse = deck.warehouses[loc["whName"]]
num_x = getattr(warehouse, "num_items_x", 0) or 0
num_y = getattr(warehouse, "num_items_y", 0) or 0
num_z = getattr(warehouse, "num_items_z", 0) or 0
if num_x <= 0 or num_y <= 0 or num_z <= 0:
continue
idx = ( idx = (
(loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y (loc.get("z", 0) - 1) * num_x * num_y
+ (loc.get("x", 0) - 1) * warehouse.num_items_x + (loc.get("y", 0) - 1) * num_x
+ (loc.get("z", 0) - 1) + (loc.get("x", 0) - 1)
) )
if 0 <= idx < warehouse.capacity: if 0 <= idx < warehouse.capacity:
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder): if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):

View File

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

View File

@@ -402,7 +402,6 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
return result_future.result return result_future.result
"""还没有改过的部分"""
def _setup_hardware_proxy( def _setup_hardware_proxy(
self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method