56 Commits

Author SHA1 Message Date
Xuwznln
52544a2c69 signal when host node is ready 2025-12-02 12:00:26 +08:00
ZiWei
5ce433e235 Fix startup with remote resource error
Resource dict fully change to "pose" key

Update oss link

Reduce pylabrobot conversion warning & force enable log dump.

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

* Updated 9320 host address

* Updated 9320 host address

* Add **kwargs in classes: PRCXI9300Deck and PRCXI9300Container

* Removed all sample_id in prcxi_9320.json to avoid KeyError

* 9320 machine testing settings

* Typo

* Typo in base_device_node.py

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

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

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

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

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

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

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

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

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

* Refactor Bioyond resource synchronization and update bottle carrier definitions

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

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

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

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

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

* support name change during materials change

* fix json dumps

* correct tip

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

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

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

* 修复 ItemizedCarrier 中的可见性逻辑

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

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

* Fix bioyond bottle_carriers ordering

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

* disable slave connect websocket

* correct remove_resource stats

* change uuid logger to trace level

* enable slave mode

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* refactor(dispensing_station): 移除wait_for_order_completion_and_get_report功能

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

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

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

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

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

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

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

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

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

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

---------

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

* remove redundant arm_slider meshes

---------

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

* 修改lh的json启动

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

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

* 添加laiyu硬件连接

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

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

* 修改laiyu移液站

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

* 更新lh以及laiyu workshop

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

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

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

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

* 修改枪头动作

* 修改虚拟仿真方法

---------

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

* 构建物料教程byxinyu

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

* Updated 9320 host address

* Updated 9320 host address

* Add **kwargs in classes: PRCXI9300Deck and PRCXI9300Container

* Removed all sample_id in prcxi_9320.json to avoid KeyError

* 9320 machine testing settings

* Typo

* Rewrite setup logic to clear error code

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

prcxi example

fix append_resource

fix regularcontainer

fix cancel error

fix resource_get param

fix json dumps

support name change during materials change

enable slave mode

change uuid logger to trace level

correct remove_resource stats

disable slave connect websocket

adjust with_children param

modify devices to use correct executor (sleep, create_task)

support sleep and create_task in node

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

bioyond_HR (#133)

* feat: Enhance Bioyond synchronization and resource management

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

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

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

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

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

Add batch creation methods for vial and solution tasks

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

convert identifier to site idx

correct extra key

update extra before transfer

fix multiple instance error

add resource_tree_transfer func

fox itemrized carrier assign child resource

support internal device material transfer

remove extra key

use same callback group

support material extra

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

prcxi样例图

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

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

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

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

add create_workflow script and test

add invisible_slots to carriers

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

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

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

More decks&plates support for bioyond (#115)

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

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

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

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

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

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

fix warehouse serialize/deserialize

fix bioyond converter

fix itemized_carrier.unassign_child_resource

allow not-loaded MSG in registry

add layout serializer & converter

warehouseuse A1-D4; add warehouse layout

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

Fix resource assignment and type mapping issues

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

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

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

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

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

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

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

* correct return message

---------

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

* Raise error when using unsupported type to create ResourceTreeSet

* Fix children key error

* Fix children key error

* Fix workstation resource not tracking

* Fix workstation deck & children resource dupe

* Fix workstation deck & children resource dupe

* Fix multiple resource error

* Fix resource tree update

* Fix resource tree update

* Force confirm uuid

* Tip more error log

* Refactor Bioyond workstation and experiment workflow (#105)

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

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

* mount parent uuid

* Add logging configuration based on BasicConfig in main function

* fix workstation node error

* fix workstation node error

* Update boot example

* temp fix for resource get

* temp fix for resource get

* provide error info when cant find plr type

* pack repo info

* fix to plr type error

* fix to plr type error

* Update regular container method

* support no size init

* fix comprehensive_station.json

* fix comprehensive_station.json

* fix type conversion

* fix state loading for regular container

* Update deploy-docs.yml

* Update deploy-docs.yml

---------

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

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

Fix param error when using mamba run

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

* delete deprecated mock devices

* rename categories

* combine chromatographic devices

* rename rviz simulation nodes

* organic virtual devices

* parse vessel_id

* run registry completion before merge

---------

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

* fix: workstation handlers and vessel_id parsing

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

* modify default discovery_interval to 15s

* feat: add trace log level

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

* fix: drop_tips not using auto resource select

* fix: discard_tips error

* fix: discard_tips

* fix: prcxi_res

* add: prcxi res
fix: startup slow

* feat: workstation example

* fix pumps and liquid_handler handle

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

* fix all protocol_compilers and remove deprecated devices

* feat: 新增use_remote_resource参数

* fix and remove redundant info

* bugfixes on organic protocols

* fix filter protocol

* fix protocol node

* 临时兼容错误的driver写法

* fix: prcxi import error

* use call_async in all service to avoid deadlock

* fix: figure_resource

* Update recipe.yaml

* add workstation template and battery example

* feat: add sk & ak

* update workstation base

* Create workstation_architecture.md

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

* refactor: ProtocolNode→WorkstationNode

* Add:msgs.action (#83)

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

* Add:msgs.action

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

* simplify resource system

* uncompleted refactor

* example for use WorkstationBase

* feat: websocket

* feat: websocket test

* feat: workstation example

* feat: action status

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

* fix: 还原protocol node处理方法

* fix: build

* fix: missing job_id key

* ws test version 1

* ws test version 2

* ws protocol

* 增加物料关系上传日志

* 增加物料关系上传日志

* 修正物料关系上传

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

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

* 修复event loop错误

* 修复edge上报错误

* 修复async错误

* 更新schema的title字段

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

* 注册表编辑器

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

* 增加addr参数

* fix: addr param

* fix: addr param

* 取消labid 和 强制config输入

* Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup

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

* Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists

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

* result_info改为字典类型

* 新增uat的地址替换

* runze multiple pump support

(cherry picked from commit 49354fcf39)

* remove runze multiple software obtainer

(cherry picked from commit 8bcc92a394)

* support multiple backbone

(cherry picked from commit 4771ff2347)

* Update runze pump format

* Correct runze multiple backbone

* Update runze_multiple_backbone

* Correct runze pump multiple receive method.

* Correct runze pump multiple receive method.

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

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

* fix import error

* fix dupe upload registry

* refactor ws client

* add server timeout

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

* fix run_column

* Update run_column_protocol.py

(cherry picked from commit e5aa4d940a)

* resource_update use resource_add

* 新增版位推荐功能

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

* update registry with nested obj

* fix protocol node log_message, added create_resource return value

* fix protocol node log_message, added create_resource return value

* try fix add protocol

* fix resource_add

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

* Feature/xprbalance-zhida (#80)

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

* feat(devices): add mettler_toledo xpr balance

* balance

* 重新补全zhida注册表

* PRCXI9320 json

* PRCXI9320 json

* PRCXI9320 json

* fix resource download

* remove class for resource

* bump version to 0.10.6

* 更新所有注册表

* 修复protocolnode的兼容性

* 修复protocolnode的兼容性

* Update install md

* Add Defaultlayout

* 更新物料接口

* fix dict to tree/nested-dict converter

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

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

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

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

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

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

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

* frontend_docs

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

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

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

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

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

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

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

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

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

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

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

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

---------

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

* 更新物料接口

* Workstation dev yb2 (#100)

* Refactor and extend reaction station action messages

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

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

* 修复to_plr_resources

* add update remove

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

* 修复资源添加

* 修复transfer_resource_to_another生成

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

* 新增test_resource动作

* fix host_node error

* fix host_node test_resource error

* fix host_node test_resource error

* 过滤本地动作

* 移动内部action以兼容host node

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

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

* update todo

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

* pass the tests

* update todo

* add conda-pack-build.yml

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

(cherry picked from commit 172599adcf)

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

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

* Update conda-pack-build.yml

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

* Update conda-pack-build.yml

* Fix FileNotFoundError

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

* Fix unilabos msgs search error

* Fix environment_check.py

* Update recipe.yaml

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

* Fix nested conda pack

* Fix one-key installation path error

* Bump version to 0.10.7

* Workshop bj (#99)

* Add LaiYu Liquid device integration and tests

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

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

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

* add

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

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

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

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

* 修复大小写文件夹名字

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

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

* Update intro.md

* 物料教程

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

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

* Update prcxi driver & fix transfer_liquid mix_times

* fix: correct mix_times type

* Update liquid_handler registry

* test: prcxi.py

* Update registry from pr

* fix ony-key script not exist

* clean files

---------

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

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: 0.10.12
version: 0.10.11
source:
path: ../unilabos

1
.gitignore vendored
View File

@@ -249,4 +249,3 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
test_config.py
Uni-Lab-OS/

View File

@@ -39,9 +39,7 @@ Uni-Lab-OS recommends using `mamba` for environment management. Choose the appro
```bash
# Create new environment
mamba create -n unilab python=3.11.11
mamba activate unilab
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
```
## Install Dev Uni-Lab-OS

View File

@@ -41,9 +41,7 @@ Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适
```bash
# 创建新环境
mamba create -n unilab python=3.11.11
mamba activate unilab
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
```
2. 安装开发版 Uni-Lab-OS:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -317,6 +317,45 @@ unilab --help
如果所有命令都正常输出,说明开发环境配置成功!
### 开发工具推荐
#### IDE
- **PyCharm Professional**: 强大的 Python IDE支持远程调试
- **VS Code**: 轻量级,配合 Python 扩展使用
- **Vim/Emacs**: 适合终端开发
#### 推荐的 VS Code 扩展
- Python
- Pylance
- ROS
- URDF
- YAML
#### 调试工具
```bash
# 安装调试工具
pip install ipdb pytest pytest-cov -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
# 代码质量检查
pip install black flake8 mypy -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
```
### 设置 pre-commit 钩子(可选)
```bash
# 安装 pre-commit
pip install pre-commit -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
# 设置钩子
pre-commit install
# 手动运行检查
pre-commit run --all-files
```
---
## 验证安装

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import json
import logging
import traceback
import uuid
import xml.etree.ElementTree as ET
from typing import Any, Dict, List
import networkx as nx
@@ -24,15 +25,7 @@ class SimpleGraph:
def add_edge(self, source, target, **attrs):
"""添加边"""
# edge = {"source": source, "target": target, **attrs}
edge = {
"source": source, "target": target,
"source_node_uuid": source,
"target_node_uuid": target,
"source_handle_io": "source",
"target_handle_io": "target",
**attrs
}
edge = {"source": source, "target": target, **attrs}
self.edges.append(edge)
def to_dict(self):
@@ -49,7 +42,6 @@ class SimpleGraph:
"multigraph": False,
"graph": {},
"nodes": nodes_list,
"edges": self.edges,
"links": self.edges,
}
@@ -66,8 +58,495 @@ def extract_json_from_markdown(text: str) -> str:
return text
def convert_to_type(val: str) -> Any:
"""将字符串值转换为适当的数据类型"""
if val == "True":
return True
if val == "False":
return False
if val == "?":
return None
if val.endswith(" g"):
return float(val.split(" ")[0])
if val.endswith("mg"):
return float(val.split("mg")[0])
elif val.endswith("mmol"):
return float(val.split("mmol")[0]) / 1000
elif val.endswith("mol"):
return float(val.split("mol")[0])
elif val.endswith("ml"):
return float(val.split("ml")[0])
elif val.endswith("RPM"):
return float(val.split("RPM")[0])
elif val.endswith(" °C"):
return float(val.split(" ")[0])
elif val.endswith(" %"):
return float(val.split(" ")[0])
return val
def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""统一的数据重构函数,根据操作类型自动选择模板"""
refactored_data = []
# 定义操作映射,包含生物实验和有机化学的所有操作
OPERATION_MAPPING = {
# 生物实验操作
"transfer_liquid": "SynBioFactory-liquid_handler.prcxi-transfer_liquid",
"transfer": "SynBioFactory-liquid_handler.biomek-transfer",
"incubation": "SynBioFactory-liquid_handler.biomek-incubation",
"move_labware": "SynBioFactory-liquid_handler.biomek-move_labware",
"oscillation": "SynBioFactory-liquid_handler.biomek-oscillation",
# 有机化学操作
"HeatChillToTemp": "SynBioFactory-workstation-HeatChillProtocol",
"StopHeatChill": "SynBioFactory-workstation-HeatChillStopProtocol",
"StartHeatChill": "SynBioFactory-workstation-HeatChillStartProtocol",
"HeatChill": "SynBioFactory-workstation-HeatChillProtocol",
"Dissolve": "SynBioFactory-workstation-DissolveProtocol",
"Transfer": "SynBioFactory-workstation-TransferProtocol",
"Evaporate": "SynBioFactory-workstation-EvaporateProtocol",
"Recrystallize": "SynBioFactory-workstation-RecrystallizeProtocol",
"Filter": "SynBioFactory-workstation-FilterProtocol",
"Dry": "SynBioFactory-workstation-DryProtocol",
"Add": "SynBioFactory-workstation-AddProtocol",
}
UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"]
for step in data:
operation = step.get("action")
if not operation or operation in UNSUPPORTED_OPERATIONS:
continue
# 处理重复操作
if operation == "Repeat":
times = step.get("times", step.get("parameters", {}).get("times", 1))
sub_steps = step.get("steps", step.get("parameters", {}).get("steps", []))
for i in range(int(times)):
sub_data = refactor_data(sub_steps)
refactored_data.extend(sub_data)
continue
# 获取模板名称
template = OPERATION_MAPPING.get(operation)
if not template:
# 自动推断模板类型
if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]:
template = f"SynBioFactory-liquid_handler.biomek-{operation}"
else:
template = f"SynBioFactory-workstation-{operation}Protocol"
# 创建步骤数据
step_data = {
"template": template,
"description": step.get("description", step.get("purpose", f"{operation} operation")),
"lab_node_type": "Device",
"parameters": step.get("parameters", step.get("action_args", {})),
}
refactored_data.append(step_data)
return refactored_data
def build_protocol_graph(
labware_info: List[Dict[str, Any]], protocol_steps: List[Dict[str, Any]], workstation_name: str
) -> SimpleGraph:
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑"""
G = SimpleGraph()
resource_last_writer = {}
LAB_NAME = "SynBioFactory"
protocol_steps = refactor_data(protocol_steps)
# 检查协议步骤中的模板来判断协议类型
has_biomek_template = any(
("biomek" in step.get("template", "")) or ("prcxi" in step.get("template", ""))
for step in protocol_steps
)
if has_biomek_template:
# 生物实验协议图构建
for labware_id, labware in labware_info.items():
node_id = str(uuid.uuid4())
labware_attrs = labware.copy()
labware_id = labware_attrs.pop("id", labware_attrs.get("name", f"labware_{uuid.uuid4()}"))
labware_attrs["description"] = labware_id
labware_attrs["lab_node_type"] = (
"Reagent" if "Plate" in str(labware_id) else "Labware" if "Rack" in str(labware_id) else "Sample"
)
labware_attrs["device_id"] = workstation_name
G.add_node(node_id, template=f"{LAB_NAME}-host_node-create_resource", **labware_attrs)
resource_last_writer[labware_id] = f"{node_id}:labware"
# 处理协议步骤
prev_node = None
for i, step in enumerate(protocol_steps):
node_id = str(uuid.uuid4())
G.add_node(node_id, **step)
# 添加控制流边
if prev_node is not None:
G.add_edge(prev_node, node_id, source_port="ready", target_port="ready")
prev_node = node_id
# 处理物料流
params = step.get("parameters", {})
if "sources" in params and params["sources"] in resource_last_writer:
source_node, source_port = resource_last_writer[params["sources"]].split(":")
G.add_edge(source_node, node_id, source_port=source_port, target_port="labware")
if "targets" in params:
resource_last_writer[params["targets"]] = f"{node_id}:labware"
# 添加协议结束节点
end_id = str(uuid.uuid4())
G.add_node(end_id, template=f"{LAB_NAME}-liquid_handler.biomek-run_protocol")
if prev_node is not None:
G.add_edge(prev_node, end_id, source_port="ready", target_port="ready")
else:
# 有机化学协议图构建
WORKSTATION_ID = workstation_name
# 为所有labware创建资源节点
for item_id, item in labware_info.items():
# item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}")
node_id = str(uuid.uuid4())
# 判断节点类型
if item.get("type") == "hardware" or "reactor" in str(item_id).lower():
if "reactor" not in str(item_id).lower():
continue
lab_node_type = "Sample"
description = f"Prepare Reactor: {item_id}"
liquid_type = []
liquid_volume = []
else:
lab_node_type = "Reagent"
description = f"Add Reagent to Flask: {item_id}"
liquid_type = [item_id]
liquid_volume = [1e5]
G.add_node(
node_id,
template=f"{LAB_NAME}-host_node-create_resource",
description=description,
lab_node_type=lab_node_type,
res_id=item_id,
device_id=WORKSTATION_ID,
class_name="container",
parent=WORKSTATION_ID,
bind_locations={"x": 0.0, "y": 0.0, "z": 0.0},
liquid_input_slot=[-1],
liquid_type=liquid_type,
liquid_volume=liquid_volume,
slot_on_deck="",
role=item.get("role", ""),
)
resource_last_writer[item_id] = f"{node_id}:labware"
last_control_node_id = None
# 处理协议步骤
for step in protocol_steps:
node_id = str(uuid.uuid4())
G.add_node(node_id, **step)
# 控制流
if last_control_node_id is not None:
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
last_control_node_id = node_id
# 物料流
params = step.get("parameters", {})
input_resources = {
"Vessel": params.get("vessel"),
"ToVessel": params.get("to_vessel"),
"FromVessel": params.get("from_vessel"),
"reagent": params.get("reagent"),
"solvent": params.get("solvent"),
"compound": params.get("compound"),
"sources": params.get("sources"),
"targets": params.get("targets"),
}
for target_port, resource_name in input_resources.items():
if resource_name and resource_name in resource_last_writer:
source_node, source_port = resource_last_writer[resource_name].split(":")
G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port)
output_resources = {
"VesselOut": params.get("vessel"),
"FromVesselOut": params.get("from_vessel"),
"ToVesselOut": params.get("to_vessel"),
"FiltrateOut": params.get("filtrate_vessel"),
"reagent": params.get("reagent"),
"solvent": params.get("solvent"),
"compound": params.get("compound"),
"sources_out": params.get("sources"),
"targets_out": params.get("targets"),
}
for source_port, resource_name in output_resources.items():
if resource_name:
resource_last_writer[resource_name] = f"{node_id}:{source_port}"
return G
def draw_protocol_graph(protocol_graph: SimpleGraph, output_path: str):
"""
(辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。
"""
if not protocol_graph:
print("Cannot draw graph: Graph object is empty.")
return
G = nx.DiGraph()
for node_id, attrs in protocol_graph.nodes.items():
label = attrs.get("description", attrs.get("template", node_id[:8]))
G.add_node(node_id, label=label, **attrs)
for edge in protocol_graph.edges:
G.add_edge(edge["source"], edge["target"])
plt.figure(figsize=(20, 15))
try:
pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
except Exception:
pos = nx.shell_layout(G) # Fallback layout
node_labels = {node: data["label"] for node, data in G.nodes(data=True)}
nx.draw(
G,
pos,
with_labels=False,
node_size=2500,
node_color="skyblue",
node_shape="o",
edge_color="gray",
width=1.5,
arrowsize=15,
)
nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold")
plt.title("Chemical Protocol Workflow Graph", size=15)
plt.savefig(output_path, dpi=300, bbox_inches="tight")
plt.close()
print(f" - Visualization saved to '{output_path}'")
from networkx.drawing.nx_agraph import to_agraph
import re
COMPASS = {"n","e","s","w","ne","nw","se","sw","c"}
def _is_compass(port: str) -> bool:
return isinstance(port, str) and port.lower() in COMPASS
def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"):
"""
使用 Graphviz 端口语法绘制协议工作流图。
- 若边上的 source_port/target_port 是 compassn/e/s/w/...),直接用 compass。
- 否则自动为节点创建 record 形状并定义命名端口 <portname>。
最终由 PyGraphviz 渲染并输出到 output_path后缀决定格式如 .png/.svg/.pdf
"""
if not protocol_graph:
print("Cannot draw graph: Graph object is empty.")
return
# 1) 先用 networkx 搭建有向图,保留端口属性
G = nx.DiGraph()
for node_id, attrs in protocol_graph.nodes.items():
label = attrs.get("description", attrs.get("template", node_id[:8]))
# 保留一个干净的“中心标签”,用于放在 record 的中间槽
G.add_node(node_id, _core_label=str(label), **{k:v for k,v in attrs.items() if k not in ("label",)})
edges_data = []
in_ports_by_node = {} # 收集命名输入端口
out_ports_by_node = {} # 收集命名输出端口
for edge in protocol_graph.edges:
u = edge["source"]
v = edge["target"]
sp = edge.get("source_port")
tp = edge.get("target_port")
# 记录到图里(保留原始端口信息)
G.add_edge(u, v, source_port=sp, target_port=tp)
edges_data.append((u, v, sp, tp))
# 如果不是 compass就按“命名端口”先归类等会儿给节点造 record
if sp and not _is_compass(sp):
out_ports_by_node.setdefault(u, set()).add(str(sp))
if tp and not _is_compass(tp):
in_ports_by_node.setdefault(v, set()).add(str(tp))
# 2) 转为 AGraph使用 Graphviz 渲染
A = to_agraph(G)
A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10")
A.node_attr.update(shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica")
A.edge_attr.update(arrowsize="0.8", color="#666666")
# 3) 为需要命名端口的节点设置 record 形状与 label
# 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口
for n in A.nodes():
node = A.get_node(n)
core = G.nodes[n].get("_core_label", n)
in_ports = sorted(in_ports_by_node.get(n, []))
out_ports = sorted(out_ports_by_node.get(n, []))
# 如果该节点涉及命名端口,则用 record否则保留原 box
if in_ports or out_ports:
def port_fields(ports):
if not ports:
return " " # 必须留一个空槽占位
# 每个端口一个小格子,<p> name
return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports)
left = port_fields(in_ports)
right = port_fields(out_ports)
# 三栏:左(入) | 中(节点名) | 右(出)
record_label = f"{{ {left} | {core} | {right} }}"
node.attr.update(shape="record", label=record_label)
else:
# 没有命名端口:普通盒子,显示核心标签
node.attr.update(label=str(core))
# 4) 给边设置 headport / tailport
# - 若端口为 compass直接用 compasse.g., headport="e"
# - 若端口为命名端口:使用在 record 中定义的 <port> 名(同名即可)
for (u, v, sp, tp) in edges_data:
e = A.get_edge(u, v)
# Graphviz 属性tail 是源head 是目标
if sp:
if _is_compass(sp):
e.attr["tailport"] = sp.lower()
else:
# 与 record label 中 <port> 名一致;特殊字符已在 label 中做了清洗
e.attr["tailport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(sp))
if tp:
if _is_compass(tp):
e.attr["headport"] = tp.lower()
else:
e.attr["headport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(tp))
# 可选:若想让边更贴边缘,可设置 constraint/spline 等
# e.attr["arrowhead"] = "vee"
# 5) 输出
A.draw(output_path, prog="dot")
print(f" - Port-aware workflow rendered to '{output_path}'")
def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]:
"""展平嵌套的XDL程序结构"""
flattened_operations = []
TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"]
def extract_operations(element: ET.Element):
if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]:
if element.tag not in TEMP_UNSUPPORTED_PROTOCOL:
flattened_operations.append(element)
for child in element:
extract_operations(child)
for child in procedure_elem:
extract_operations(child)
return flattened_operations
def parse_xdl_content(xdl_content: str) -> tuple:
"""解析XDL内容"""
try:
xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable())
root = ET.fromstring(xdl_content_cleaned)
synthesis_elem = root.find("Synthesis")
if synthesis_elem is None:
return None, None, None
# 解析硬件组件
hardware_elem = synthesis_elem.find("Hardware")
hardware = []
if hardware_elem is not None:
hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")]
# 解析试剂
reagents_elem = synthesis_elem.find("Reagents")
reagents = []
if reagents_elem is not None:
reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")]
# 解析程序
procedure_elem = synthesis_elem.find("Procedure")
if procedure_elem is None:
return None, None, None
flattened_operations = flatten_xdl_procedure(procedure_elem)
return hardware, reagents, flattened_operations
except ET.ParseError as e:
raise ValueError(f"Invalid XDL format: {e}")
def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]:
"""
将XDL XML格式转换为标准的字典格式
Args:
xdl_content: XDL XML内容
Returns:
转换结果,包含步骤和器材信息
"""
try:
hardware, reagents, flattened_operations = parse_xdl_content(xdl_content)
if hardware is None:
return {"error": "Failed to parse XDL content", "success": False}
# 将XDL元素转换为字典格式
steps_data = []
for elem in flattened_operations:
# 转换参数类型
parameters = {}
for key, val in elem.attrib.items():
converted_val = convert_to_type(val)
if converted_val is not None:
parameters[key] = converted_val
step_dict = {
"operation": elem.tag,
"parameters": parameters,
"description": elem.get("purpose", f"Operation: {elem.tag}"),
}
steps_data.append(step_dict)
# 合并硬件和试剂为统一的labware_info格式
labware_data = []
labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware)
labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents)
return {
"success": True,
"steps": steps_data,
"labware": labware_data,
"message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.",
}
except Exception as e:
error_msg = f"XDL conversion failed: {str(e)}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
def create_workflow(

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
import sys
from datetime import datetime
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
import pytest
from unilabos.workflow.convert_from_json import (
convert_from_json,
normalize_steps as _normalize_steps,
normalize_labware as _normalize_labware,
)
from unilabos.workflow.common import draw_protocol_graph_with_ports
@pytest.mark.parametrize(
"protocol_name",
[
"example_bio",
# "bioyond_materials_liquidhandling_1",
"example_prcxi",
],
)
def test_build_protocol_graph(protocol_name):
data_path = Path(__file__).with_name(f"{protocol_name}.json")
graph = convert_from_json(data_path, workstation_name="PRCXi")
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
output_path = data_path.with_name(f"{protocol_name}_graph_{timestamp}.png")
draw_protocol_graph_with_ports(graph, str(output_path))
print(graph)

View File

@@ -1 +1 @@
__version__ = "0.10.12"
__version__ = "0.10.11"

View File

@@ -141,7 +141,7 @@ class CommunicationClientFactory:
"""
if cls._client_cache is None:
cls._client_cache = cls.create_client(protocol)
logger.trace(f"[CommunicationFactory] Created {type(cls._client_cache).__name__} client")
logger.info(f"[CommunicationFactory] Created {type(cls._client_cache).__name__} client")
return cls._client_cache

View File

@@ -20,7 +20,6 @@ if unilabos_dir not in sys.path:
from unilabos.utils.banner_print import print_status, print_unilab_banner
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
def load_config_from_file(config_path):
if config_path is None:
config_path = os.environ.get("UNILABOS_BASICCONFIG_CONFIG_PATH", None)
@@ -42,7 +41,7 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
for i, arg in enumerate(sys.argv):
for option_string in option_strings:
if arg.startswith(option_string):
new_arg = arg[:2] + arg[2 : len(option_string)].replace("-", "_") + arg[len(option_string) :]
new_arg = arg[:2] + arg[2:len(option_string)].replace("-", "_") + arg[len(option_string):]
sys.argv[i] = new_arg
break
@@ -50,8 +49,6 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
subparsers = parser.add_subparsers(title="Valid subcommands", dest="command")
parser.add_argument("-g", "--graph", help="Physical setup graph file path.")
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file path.")
parser.add_argument(
@@ -156,54 +153,21 @@ def parse_args():
default=False,
help="Complete registry information",
)
# workflow upload subcommand
workflow_parser = subparsers.add_parser(
"workflow_upload",
aliases=["wf"],
help="Upload workflow from xdl/json/python files",
)
workflow_parser.add_argument(
"-f",
"--workflow_file",
type=str,
required=True,
help="Path to the workflow file (JSON format)",
)
workflow_parser.add_argument(
"-n",
"--workflow_name",
type=str,
default=None,
help="Workflow name, if not provided will use the name from file or filename",
)
workflow_parser.add_argument(
"--tags",
type=str,
nargs="*",
default=[],
help="Tags for the workflow (space-separated)",
)
workflow_parser.add_argument(
"--published",
action="store_true",
default=False,
help="Whether to publish the workflow (default: False)",
)
return parser
def main():
"""主函数"""
# 解析命令行参数
parser = parse_args()
convert_argv_dashes_to_underscores(parser)
args = parser.parse_args()
args_dict = vars(args)
args = parse_args()
convert_argv_dashes_to_underscores(args)
args_dict = vars(args.parse_args())
# 环境检查 - 检查并自动安装必需的包 (可选)
if not args_dict.get("skip_env_check", False):
from unilabos.utils.environment_check import check_environment
print_status("正在进行环境依赖检查...", "info")
if not check_environment(auto_install=True):
print_status("环境检查失败,程序退出", "error")
os._exit(1)
@@ -256,18 +220,17 @@ def main():
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
if args.addr != parser.get_default("addr"):
if args.addr == "test":
print_status("使用测试环境地址", "info")
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
elif args.addr == "uat":
print_status("使用uat环境地址", "info")
HTTPConfig.remote_addr = "https://uni-lab.uat.bohrium.com/api/v1"
elif args.addr == "local":
print_status("使用本地环境地址", "info")
HTTPConfig.remote_addr = "http://127.0.0.1:48197/api/v1"
else:
HTTPConfig.remote_addr = args.addr
if args_dict["addr"] == "test":
print_status("使用测试环境地址", "info")
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
elif args_dict["addr"] == "uat":
print_status("使用uat环境地址", "info")
HTTPConfig.remote_addr = "https://uni-lab.uat.bohrium.com/api/v1"
elif args_dict["addr"] == "local":
print_status("使用本地环境地址", "info")
HTTPConfig.remote_addr = "http://127.0.0.1:48197/api/v1"
else:
HTTPConfig.remote_addr = args_dict.get("addr", "")
# 设置BasicConfig参数
if args_dict.get("ak", ""):
@@ -276,12 +239,9 @@ def main():
if args_dict.get("sk", ""):
BasicConfig.sk = args_dict.get("sk", "")
print_status("传入了sk参数优先采用传入参数", "info")
BasicConfig.working_dir = working_dir
workflow_upload = args_dict.get("command") in ("workflow_upload", "wf")
# 使用远程资源启动
if not workflow_upload and args_dict["use_remote_resource"]:
if args_dict["use_remote_resource"]:
print_status("使用远程资源启动", "info")
from unilabos.app.web import http_client
@@ -294,6 +254,7 @@ def main():
BasicConfig.port = args_dict["port"] if args_dict["port"] else BasicConfig.port
BasicConfig.disable_browser = args_dict["disable_browser"] or BasicConfig.disable_browser
BasicConfig.working_dir = working_dir
BasicConfig.is_host_mode = not args_dict.get("is_slave", False)
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
@@ -322,31 +283,9 @@ def main():
# 注册表
lab_registry = build_registry(
args_dict["registry_path"], args_dict.get("complete_registry", False), BasicConfig.upload_registry
args_dict["registry_path"], args_dict.get("complete_registry", False), args_dict["upload_registry"]
)
if BasicConfig.upload_registry:
# 设备注册到服务端 - 需要 ak 和 sk
if BasicConfig.ak and BasicConfig.sk:
print_status("开始注册设备到服务端...", "info")
try:
register_devices_and_resources(lab_registry)
print_status("设备注册完成", "info")
except Exception as e:
print_status(f"设备注册失败: {e}", "error")
else:
print_status("未提供 ak 和 sk跳过设备注册", "info")
else:
print_status("本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning")
# 处理 workflow_upload 子命令
if workflow_upload:
from unilabos.workflow.wf_utils import handle_workflow_upload_command
handle_workflow_upload_command(args_dict)
print_status("工作流上传完成,程序退出", "info")
os._exit(0)
if not BasicConfig.ak or not BasicConfig.sk:
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
os._exit(1)
@@ -367,37 +306,10 @@ def main():
graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json)
else:
if not os.path.isfile(file_path):
# 尝试从 main.py 向上两级目录查找
temp_file_path = os.path.abspath(str(os.path.join(__file__, "..", "..", file_path)))
if os.path.isfile(temp_file_path):
print_status(f"使用相对路径{temp_file_path}", "info")
file_path = temp_file_path
else:
# 尝试在 working_dir 中查找
working_dir_file_path = os.path.join(working_dir, file_path)
if os.path.isfile(working_dir_file_path):
print_status(f"在工作目录中找到文件: {working_dir_file_path}", "info")
file_path = working_dir_file_path
else:
# 尝试使用文件名在 working_dir 中查找
file_name = os.path.basename(file_path)
working_dir_file_path = os.path.join(working_dir, file_name)
if os.path.isfile(working_dir_file_path):
print_status(f"在工作目录中找到文件: {working_dir_file_path}", "info")
file_path = working_dir_file_path
# 最终检查文件是否存在
if not os.path.isfile(file_path):
print_status(
f"无法找到设备加载文件: {file_path}\n"
f"已尝试在以下位置查找:\n"
f" 1. 原始路径: {args_dict.get('graph', BasicConfig.startup_json_path)}\n"
f" 2. 相对路径: {os.path.abspath(str(os.path.join(__file__, '..', '..', args_dict.get('graph', BasicConfig.startup_json_path) or '')))}\n"
f" 3. 工作目录: {os.path.join(working_dir, args_dict.get('graph', BasicConfig.startup_json_path) or '')}\n"
f" 4. 工作目录(仅文件名): {os.path.join(working_dir, os.path.basename(args_dict.get('graph', BasicConfig.startup_json_path) or ''))}\n"
f"请使用 -g 参数指定正确的文件路径,或在工作目录 {working_dir} 中放置文件",
"error"
)
os._exit(1)
if file_path.endswith(".json"):
graph, resource_tree_set, resource_links = read_node_link_json(file_path)
else:
@@ -450,6 +362,20 @@ def main():
args_dict["devices_config"] = resource_tree_set
args_dict["graph"] = graph_res.physical_setup_graph
if BasicConfig.upload_registry:
# 设备注册到服务端 - 需要 ak 和 sk
if BasicConfig.ak and BasicConfig.sk:
print_status("开始注册设备到服务端...", "info")
try:
register_devices_and_resources(lab_registry)
print_status("设备注册完成", "info")
except Exception as e:
print_status(f"设备注册失败: {e}", "error")
else:
print_status("未提供 ak 和 sk跳过设备注册", "info")
else:
print_status("本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning")
if args_dict["controllers"] is not None:
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
else:
@@ -464,7 +390,6 @@ def main():
comm_client = get_communication_client()
if "websocket" in args_dict["app_bridges"]:
args_dict["bridges"].append(comm_client)
def _exit(signum, frame):
comm_client.stop()
sys.exit(0)
@@ -506,13 +431,16 @@ def main():
resource_visualization.start()
except OSError as e:
if "AMENT_PREFIX_PATH" in str(e):
print_status(f"ROS 2环境未正确设置跳过3D可视化启动。错误详情: {e}", "warning")
print_status(
f"ROS 2环境未正确设置跳过3D可视化启动。错误详情: {e}",
"warning"
)
print_status(
"建议解决方案:\n"
"1. 激活Conda环境: conda activate unilab\n"
"2. 或使用 --backend simple 参数\n"
"3. 或使用 --visual disable 参数禁用可视化",
"info",
"info"
)
else:
raise

View File

@@ -76,8 +76,7 @@ class HTTPClient:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
payload = {"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}
f.write(json.dumps(payload, indent=4))
f.write(json.dumps({"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, indent=4))
# 从序列化数据中提取所有节点的UUID保存旧UUID
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
if not self.initialized or first_add:
@@ -332,67 +331,6 @@ class HTTPClient:
logger.error(f"响应内容: {response.text}")
return None
def workflow_import(
self,
name: str,
workflow_uuid: str,
workflow_name: str,
nodes: List[Dict[str, Any]],
edges: List[Dict[str, Any]],
tags: Optional[List[str]] = None,
published: bool = False,
) -> Dict[str, Any]:
"""
导入工作流到服务器
Args:
name: 工作流名称(顶层)
workflow_uuid: 工作流UUID
workflow_name: 工作流名称data内部
nodes: 工作流节点列表
edges: 工作流边列表
tags: 工作流标签列表,默认为空列表
published: 是否发布工作流默认为False
Returns:
Dict: API响应数据包含 code 和 data (uuid, name)
"""
# target_lab_uuid 暂时使用默认值,后续由后端根据 ak/sk 获取
payload = {
"target_lab_uuid": "28c38bb0-63f6-4352-b0d8-b5b8eb1766d5",
"name": name,
"data": {
"workflow_uuid": workflow_uuid,
"workflow_name": workflow_name,
"nodes": nodes,
"edges": edges,
"tags": tags if tags is not None else [],
"published": published,
},
}
# 保存请求到文件
with open(os.path.join(BasicConfig.working_dir, "req_workflow_upload.json"), "w", encoding="utf-8") as f:
f.write(json.dumps(payload, indent=4, ensure_ascii=False))
response = requests.post(
f"{self.remote_addr}/lab/workflow/owner/import",
json=payload,
headers={"Authorization": f"Lab {self.auth}"},
timeout=60,
)
# 保存响应到文件
with open(os.path.join(BasicConfig.working_dir, "res_workflow_upload.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"导入工作流失败: {response.text}")
return res
else:
logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
return {"code": response.status_code, "message": response.text}
# 创建默认客户端实例
http_client = HTTPClient()

View File

@@ -389,7 +389,7 @@ class MessageProcessor:
self.is_running = True
self.thread = threading.Thread(target=self._run, daemon=True, name="MessageProcessor")
self.thread.start()
logger.trace("[MessageProcessor] Started")
logger.info("[MessageProcessor] Started")
def stop(self) -> None:
"""停止消息处理线程"""
@@ -421,7 +421,7 @@ class MessageProcessor:
ssl_context = ssl_module.create_default_context()
ws_logger = logging.getLogger("websockets.client")
# 日志级别已在 unilabos.utils.log 中统一配置为 WARNING
ws_logger.setLevel(logging.INFO)
async with websockets.connect(
self.websocket_url,
@@ -438,7 +438,7 @@ class MessageProcessor:
self.connected = True
self.reconnect_count = 0
logger.trace(f"[MessageProcessor] Connected to {self.websocket_url}")
logger.info(f"[MessageProcessor] Connected to {self.websocket_url}")
# 启动发送协程
send_task = asyncio.create_task(self._send_handler())
@@ -503,7 +503,7 @@ class MessageProcessor:
async def _send_handler(self):
"""处理发送队列中的消息"""
logger.trace("[MessageProcessor] Send handler started")
logger.debug("[MessageProcessor] Send handler started")
try:
while self.connected and self.websocket:
@@ -939,7 +939,7 @@ class QueueProcessor:
# 事件通知机制
self.queue_update_event = threading.Event()
logger.trace("[QueueProcessor] Initialized")
logger.info("[QueueProcessor] Initialized")
def set_websocket_client(self, websocket_client: "WebSocketClient"):
"""设置WebSocket客户端引用"""
@@ -954,7 +954,7 @@ class QueueProcessor:
self.is_running = True
self.thread = threading.Thread(target=self._run, daemon=True, name="QueueProcessor")
self.thread.start()
logger.trace("[QueueProcessor] Started")
logger.info("[QueueProcessor] Started")
def stop(self) -> None:
"""停止队列处理线程"""
@@ -965,7 +965,7 @@ class QueueProcessor:
def _run(self):
"""运行队列处理主循环"""
logger.trace("[QueueProcessor] Queue processor started")
logger.debug("[QueueProcessor] Queue processor started")
while self.is_running:
try:
@@ -1175,6 +1175,7 @@ class WebSocketClient(BaseCommunicationClient):
else:
url = f"{scheme}://{parsed.netloc}/api/v1/ws/schedule"
logger.debug(f"[WebSocketClient] URL: {url}")
return url
def start(self) -> None:
@@ -1187,11 +1188,13 @@ class WebSocketClient(BaseCommunicationClient):
logger.error("[WebSocketClient] WebSocket URL not configured")
return
logger.info(f"[WebSocketClient] Starting connection to {self.websocket_url}")
# 启动两个核心线程
self.message_processor.start()
self.queue_processor.start()
logger.trace("[WebSocketClient] All threads started")
logger.info("[WebSocketClient] All threads started")
def stop(self) -> None:
"""停止WebSocket客户端"""
@@ -1240,7 +1243,7 @@ class WebSocketClient(BaseCommunicationClient):
},
}
self.message_processor.send_message(message)
logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
logger.debug(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
def publish_job_status(
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None

View File

@@ -21,8 +21,7 @@ class BasicConfig:
startup_json_path = None # 填写绝对路径
disable_browser = False # 禁止浏览器自动打开
port = 8002 # 本地HTTP服务
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
log_level: Literal['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = "DEBUG" # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
@classmethod
def auth_secret(cls):
@@ -42,7 +41,7 @@ class WSConfig:
# HTTP配置
class HTTPConfig:
remote_addr = "https://uni-lab.bohrium.com/api/v1"
remote_addr = "http://127.0.0.1:48197/api/v1"
# ROS配置
@@ -66,14 +65,13 @@ def _update_config_from_module(module):
if not attr.startswith("_"):
setattr(obj, attr, getattr(getattr(module, name), attr))
def _update_config_from_env():
prefix = "UNILABOS_"
for env_key, env_value in os.environ.items():
if not env_key.startswith(prefix):
continue
try:
key_path = env_key[len(prefix) :] # Remove UNILAB_ prefix
key_path = env_key[len(prefix):] # Remove UNILAB_ prefix
class_field = key_path.upper().split("_", 1)
if len(class_field) != 2:
logger.warning(f"[ENV] 环境变量格式不正确:{env_key}")

View File

@@ -0,0 +1,29 @@
{
"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

@@ -13,8 +13,6 @@
- 状态类型: working/stop/finish/protect/pause/false/unknown
"""
import os
import sys
import socket
import xml.etree.ElementTree as ET
import json
@@ -23,6 +21,7 @@ from dataclasses import dataclass
from typing import Any, Dict, List, Optional, TypedDict
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.presets.workstation import ROS2WorkstationNode
@@ -57,6 +56,13 @@ class BatteryTestPositionState(TypedDict):
status: 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):
@@ -136,9 +142,9 @@ class NewareBatteryTestSystem:
devtype: str = None,
timeout: int = None,
size_x: float = 50,
size_y: float = 50,
size_z: float = 20,
size_x: float = 500.0,
size_y: float = 500.0,
size_z: float = 2000.0,
):
"""
初始化新威电池测试系统
@@ -156,12 +162,6 @@ class NewareBatteryTestSystem:
self.machine_id = machine_id
self.devtype = devtype or self.DEVTYPE
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._cached_status = {}
self._ros_node: Optional[ROS2WorkstationNode] = None # ROS节点引用由框架设置
@@ -192,9 +192,8 @@ class NewareBatteryTestSystem:
def _setup_material_management(self):
"""设置物料管理系统"""
# 第1盘5行8列网格 (A1-E8) - 5行对应subdevid 1-58列对应chlid 1-8
# 先给物料设置一个最大的Deck,并设置其在空间中的位置
deck_main = Deck("ADeckName", 2000, 1800, 100, origin=Coordinate(2000,2000,0))
# 先给物料设置一个最大的Deck
deck_main = Deck("ADeckName", 200, 200, 200)
plate1_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d(
BatteryTestPosition,
@@ -203,8 +202,8 @@ class NewareBatteryTestSystem:
dx=10,
dy=10,
dz=0,
item_dx=65,
item_dy=65
item_dx=45,
item_dy=45
)
plate1 = Plate("P1", 400, 300, 50, ordered_items=plate1_resources)
deck_main.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
@@ -233,15 +232,11 @@ class NewareBatteryTestSystem:
num_items_y=5, # 5行对应subdevid 6-10即A-E
dx=10,
dy=10,
dz=0,
dz=100, # Z轴偏移100mm
item_dx=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_前缀
self.station_resources_plate2 = {}
for name, resource in plate2_resources.items():
@@ -311,132 +306,55 @@ class NewareBatteryTestSystem:
def _update_plate_resources(self, subunits: Dict):
"""更新两盘电池资源的状态"""
# 第1盘subdevid 1-5 映射到 8列5行网格 (列0-7, 行0-4)
# 第1盘subdevid 1-5 映射到 P1_A1-P1_E8 (5行8列)
for subdev_id in range(1, 6): # subdevid 1-5
status_row = subunits.get(subdev_id, {})
for chl_id in range(1, 9): # chlid 1-8
try:
# 根据用户描述:第一个是(0,0),最后一个是(7,4)
# 说明是8列5行列从0开始行从0开始
col_idx = (chl_id - 1) # 0-7 (chlid 1-8 -> 列0-7)
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
# 计算在5×8网格中的位置
row_idx = (subdev_id - 1) # 0-4 (对应A-E)
col_idx = (chl_id - 1) # 0-7 (对应1-8)
resource_name = f"P1_{self.LETTERS[row_idx]}{col_idx + 1}"
r = self.station_resources.get(resource_name)
if r:
status_channel = status_row.get(chl_id, {})
metrics = status_channel.get("metrics", {})
# 构建BatteryTestPosition状态数据移除capacity和energy
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"),
"color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
# 通道名称标识
"Channel_Name": f"{self.machine_id}-{subdev_id}-{chl_id}",
"voltage": status_channel.get("voltage_V", 0.0),
"current": status_channel.get("current_A", 0.0),
"time": status_channel.get("totaltime_s", 0.0),
}
r.load_state(channel_state)
# 调试信息
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}")
except (KeyError, IndexError):
continue
# 第2盘subdevid 6-10 映射到 8列5行网格 (列0-7, 行0-4)
# 第2盘subdevid 6-10 映射到 P2_A1-P2_E8 (5行8列)
for subdev_id in range(6, 11): # subdevid 6-10
status_row = subunits.get(subdev_id, {})
for chl_id in range(1, 9): # chlid 1-8
try:
col_idx = (chl_id - 1) # 0-7 (chlid 1-8 -> 列0-7)
row_idx = (subdev_id - 6) # 0-4 (subdevid 6-10 -> 行0-4)
# 尝试多种可能的资源命名格式
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
# 计算在5×8网格中的位置
row_idx = (subdev_id - 6) # 0-4 (subdevid 6->0, 7->1, ..., 10->4) (对应A-E)
col_idx = (chl_id - 1) # 0-7 (对应1-8)
resource_name = f"P2_{self.LETTERS[row_idx]}{col_idx + 1}"
r = self.station_resources.get(resource_name)
if r:
status_channel = status_row.get(chl_id, {})
metrics = status_channel.get("metrics", {})
# 构建BatteryTestPosition状态数据移除capacity和energy
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"),
"color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
# 通道名称标识
"Channel_Name": f"{self.machine_id}-{subdev_id}-{chl_id}",
"voltage": status_channel.get("voltage_V", 0.0),
"current": status_channel.get("current_A", 0.0),
"time": status_channel.get("totaltime_s", 0.0),
}
r.load_state(channel_state)
# 调试信息
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}")
except (KeyError, IndexError):
continue
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": list(self.station_resources.values())
})
@property
def connection_info(self) -> Dict[str, str]:
@@ -572,45 +490,6 @@ 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}
# ========================
# 辅助方法
# ========================
@@ -659,228 +538,6 @@ class NewareBatteryTestSystem:
except Exception as 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:
"""
获取设备级别的摘要统计设备动作

View File

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

View File

@@ -989,18 +989,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
else:
dis_vols = [float(v) for v in dis_vols]
# 统一混合次数为标量,防止数组/列表与 int 比较时报错
if mix_times is not None and not isinstance(mix_times, (int, float)):
try:
mix_times = mix_times[0] if len(mix_times) > 0 else None
except Exception:
try:
mix_times = next(iter(mix_times))
except Exception:
pass
if mix_times is not None:
mix_times = int(mix_times)
# 识别传输模式
num_sources = len(sources)
num_targets = len(targets)

View File

@@ -5,7 +5,6 @@ import json
import os
import socket
import time
import uuid
from typing import Any, List, Dict, Optional, Tuple, TypedDict, Union, Sequence, Iterator, Literal
from pylabrobot.liquid_handling import (
@@ -857,30 +856,7 @@ class PRCXI9300Api:
def _raw_request(self, payload: str) -> str:
if self.debug:
# 调试/仿真模式下直接返回可解析的模拟 JSON避免后续 json.loads 报错
try:
req = json.loads(payload)
method = req.get("MethodName")
except Exception:
method = None
data: Any = True
if method in {"AddSolution"}:
data = str(uuid.uuid4())
elif method in {"AddWorkTabletMatrix", "AddWorkTabletMatrix2"}:
data = {"Success": True, "Message": "debug mock"}
elif method in {"GetErrorCode"}:
data = ""
elif method in {"RemoveErrorCodet", "Reset", "Start", "LoadSolution", "Pause", "Resume", "Stop"}:
data = True
elif method in {"GetStepStateList", "GetStepStatus", "GetStepState"}:
data = []
elif method in {"GetLocation"}:
data = {"X": 0, "Y": 0, "Z": 0}
elif method in {"GetResetStatus"}:
data = False
return json.dumps({"Success": True, "Msg": "debug mock", "Data": data})
return " "
with contextlib.closing(socket.socket()) as sock:
sock.settimeout(self.timeout)
sock.connect((self.host, self.port))

View File

@@ -1,8 +0,0 @@
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

@@ -1,3 +0,0 @@
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

@@ -1,33 +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.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": []
}

View File

@@ -1,49 +0,0 @@
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 +0,0 @@
material_name
LiPF6
LiDFOB
DTD
LiFSI
LiPO2F2
1 material_name
2 LiPF6
3 LiDFOB
4 DTD
5 LiFSI
6 LiPO2F2

View File

@@ -47,8 +47,8 @@ class BioyondV1RPC(BaseRequest):
super().__init__()
print("开始初始化 BioyondV1RPC")
self.config = config
self.api_key = config.get("api_key", "")
self.host = config.get("api_host", "") or config.get("base_url", "")
self.api_key = config["api_key"]
self.host = config["api_host"]
self._logger = SimpleLogger()
self.material_cache = {}
self._load_material_cache()
@@ -61,7 +61,7 @@ class BioyondV1RPC(BaseRequest):
:return: 当前时间的 ISO 8601 格式字符串
"""
current_time = datetime.now().isoformat(
current_time = datetime.now(timezone.utc).isoformat(
timespec='milliseconds'
)
# 替换时区部分为 'Z'
@@ -192,6 +192,23 @@ class BioyondV1RPC(BaseRequest):
return []
return str(response.get("data", {}))
def material_type_list(self) -> list:
"""查询物料类型列表
返回值:
list: 物料类型数组,失败返回空列表
"""
response = self.post(
url=f'{self.host}/api/lims/storage/material-type-list',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": {},
})
if not response or response['code'] != 1:
return []
return response.get("data", [])
def material_inbound(self, material_id: str, location_id: str) -> dict:
"""
描述:指定库位入库一个物料
@@ -212,8 +229,34 @@ class BioyondV1RPC(BaseRequest):
})
if not response or response['code'] != 1:
if response:
error_msg = response.get('message', '未知错误')
print(f"[ERROR] 物料入库失败: code={response.get('code')}, message={error_msg}")
else:
print(f"[ERROR] 物料入库失败: API 无响应")
return {}
return response.get("data", {})
# 入库成功时,即使没有 data 字段,也返回成功标识
return response.get("data") or {"success": True}
def batch_inbound(self, inbound_items: List[Dict[str, Any]]) -> int:
"""批量入库物料
参数:
inbound_items: 入库条目列表,每项包含 materialId/locationId/quantity 等
返回值:
int: 成功返回1失败返回0
"""
response = self.post(
url=f'{self.host}/api/lims/storage/batch-inbound',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": inbound_items,
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
def delete_material(self, material_id: str) -> dict:
"""
@@ -233,7 +276,7 @@ class BioyondV1RPC(BaseRequest):
return response.get("data", {})
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
"""指定库位出库物料"""
"""指定库位出库物料(通过库位名称)"""
location_id = LOCATION_MAPPING.get(location_name, location_name)
params = {
@@ -251,9 +294,98 @@ class BioyondV1RPC(BaseRequest):
})
if not response or response['code'] != 1:
return {}
return None
return response
def material_outbound_by_id(self, material_id: str, location_id: str, quantity: int) -> dict:
"""指定库位出库物料直接使用location_id
Args:
material_id: 物料ID
location_id: 库位ID不是库位名称是UUID
quantity: 数量
Returns:
dict: API响应失败返回None
"""
params = {
"materialId": material_id,
"locationId": location_id,
"quantity": quantity
}
response = self.post(
url=f'{self.host}/api/lims/storage/outbound',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": params
})
if not response or response['code'] != 1:
return None
return response
def batch_outbound(self, outbound_items: List[Dict[str, Any]]) -> int:
"""批量出库物料
参数:
outbound_items: 出库条目列表,每项包含 materialId/locationId/quantity 等
返回值:
int: 成功返回1失败返回0
"""
response = self.post(
url=f'{self.host}/api/lims/storage/batch-outbound',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": outbound_items,
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
def material_info(self, material_id: str) -> dict:
"""查询物料详情
参数:
material_id: 物料ID
返回值:
dict: 物料信息字典,失败返回空字典
"""
response = self.post(
url=f'{self.host}/api/lims/storage/material-info',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": material_id,
})
if not response or response['code'] != 1:
return {}
return response.get("data", {})
def reset_location(self, location_id: str) -> int:
"""复位库位
参数:
location_id: 库位ID
返回值:
int: 成功返回1失败返回0
"""
response = self.post(
url=f'{self.host}/api/lims/storage/reset-location',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": location_id,
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
# ==================== 工作流查询相关接口 ====================
def query_workflow(self, json_str: str) -> dict:
@@ -297,6 +429,66 @@ class BioyondV1RPC(BaseRequest):
return {}
return response.get("data", {})
def split_workflow_list(self, params: Dict[str, Any]) -> dict:
"""查询可拆分工作流列表
参数:
params: 查询条件参数
返回值:
dict: 返回数据字典,失败返回空字典
"""
response = self.post(
url=f'{self.host}/api/lims/workflow/split-workflow-list',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": params,
})
if not response or response['code'] != 1:
return {}
return response.get("data", {})
def merge_workflow(self, data: Dict[str, Any]) -> dict:
"""合并工作流(无参数版)
参数:
data: 合并请求体,包含待合并的子工作流信息
返回值:
dict: 合并结果,失败返回空字典
"""
response = self.post(
url=f'{self.host}/api/lims/workflow/merge-workflow',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": data,
})
if not response or response['code'] != 1:
return {}
return response.get("data", {})
def merge_workflow_with_parameters(self, data: Dict[str, Any]) -> dict:
"""合并工作流(携带参数)
参数:
data: 合并请求体,包含 name、workflows 以及 stepParameters 等
返回值:
dict: 合并结果,失败返回空字典
"""
response = self.post(
url=f'{self.host}/api/lims/workflow/merge-workflow-with-parameters',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": data,
})
if not response or response['code'] != 1:
return {}
return response.get("data", {})
def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]:
"""验证工作流参数格式"""
try:
@@ -459,18 +651,15 @@ class BioyondV1RPC(BaseRequest):
return {}
return response.get("data", {})
def order_report(self, json_str: str) -> dict:
"""
描述:查询某个任务明细
json_str 格式为JSON字符串:
'{"order_id": "order123"}'
"""
try:
data = json.loads(json_str)
order_id = data.get("order_id", "")
except json.JSONDecodeError:
return {}
def order_report(self, order_id: str) -> dict:
"""查询订单报告
参数:
order_id: 订单ID
返回值:
dict: 报告数据,失败返回空字典
"""
response = self.post(
url=f'{self.host}/api/lims/order/order-report',
params={
@@ -478,16 +667,18 @@ class BioyondV1RPC(BaseRequest):
"requestTime": self.get_current_time_iso8601(),
"data": order_id,
})
if not response or response['code'] != 1:
return {}
return response.get("data", {})
def order_takeout(self, json_str: str) -> int:
"""
描述:取出任务产物
json_str 格式为JSON字符串:
'{"order_id": "order123", "preintake_id": "preintake123"}'
"""取出任务产物
参数:
json_str: JSON字符串包含 order_id 与 preintake_id
返回值:
int: 成功返回1失败返回0
"""
try:
data = json.loads(json_str)
@@ -510,14 +701,15 @@ class BioyondV1RPC(BaseRequest):
return 0
return response.get("code", 0)
def sample_waste_removal(self, order_id: str) -> dict:
"""
样品/废料取出接口
"""样品/废料取出
参数:
- order_id: 订单ID
order_id: 订单ID
返回: 取出结果
返回:
dict: 取出结果,失败返回空字典
"""
params = {"orderId": order_id}
@@ -539,10 +731,13 @@ class BioyondV1RPC(BaseRequest):
return response.get("data", {})
def cancel_order(self, json_str: str) -> bool:
"""
描述:取消指定任务
json_str 格式为JSON字符串:
'{"order_id": "order123"}'
"""取消指定任务
参数:
json_str: JSON字符串包含 order_id
返回值:
bool: 成功返回 True失败返回 False
"""
try:
data = json.loads(json_str)
@@ -562,6 +757,126 @@ class BioyondV1RPC(BaseRequest):
return False
return True
def cancel_experiment(self, order_id: str) -> int:
"""取消指定实验
参数:
order_id: 订单ID
返回值:
int: 成功返回1失败返回0
"""
response = self.post(
url=f'{self.host}/api/lims/order/cancel-experiment',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": order_id,
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
def batch_cancel_experiment(self, order_ids: List[str]) -> int:
"""批量取消实验
参数:
order_ids: 订单ID列表
返回值:
int: 成功返回1失败返回0
"""
response = self.post(
url=f'{self.host}/api/lims/order/batch-cancel-experiment',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": order_ids,
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
def gantts_by_order_id(self, order_id: str) -> dict:
"""查询订单甘特图数据
参数:
order_id: 订单ID
返回值:
dict: 甘特数据,失败返回空字典
"""
response = self.post(
url=f'{self.host}/api/lims/order/gantts-by-order-id',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": order_id,
})
if not response or response['code'] != 1:
return {}
return response.get("data", {})
def simulation_gantt_by_order_id(self, order_id: str) -> dict:
"""查询订单模拟甘特图数据
参数:
order_id: 订单ID
返回值:
dict: 模拟甘特数据,失败返回空字典
"""
response = self.post(
url=f'{self.host}/api/lims/order/simulation-gantt-by-order-id',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": order_id,
})
if not response or response['code'] != 1:
return {}
return response.get("data", {})
def reset_order_status(self, order_id: str) -> int:
"""复位订单状态
参数:
order_id: 订单ID
返回值:
int: 成功返回1失败返回0
"""
response = self.post(
url=f'{self.host}/api/lims/order/reset-order-status',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": order_id,
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
def gantt_with_simulation_by_order_id(self, order_id: str) -> dict:
"""查询订单甘特与模拟联合数据
参数:
order_id: 订单ID
返回值:
dict: 联合数据,失败返回空字典
"""
response = self.post(
url=f'{self.host}/api/lims/order/gantt-with-simulation-by-order-id',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": order_id,
})
if not response or response['code'] != 1:
return {}
return response.get("data", {})
# ==================== 设备管理相关接口 ====================
def device_list(self, json_str: str = "") -> list:
@@ -593,9 +908,13 @@ class BioyondV1RPC(BaseRequest):
return response.get("data", [])
def device_operation(self, json_str: str) -> int:
"""
描述:操作设备
json_str 格式为JSON字符串
"""设备操作
参数:
json_str: JSON字符串包含 device_no/operationType/operationParams
返回值:
int: 成功返回1失败返回0
"""
try:
data = json.loads(json_str)
@@ -608,7 +927,7 @@ class BioyondV1RPC(BaseRequest):
return 0
response = self.post(
url=f'{self.host}/api/lims/device/device-operation',
url=f'{self.host}/api/lims/device/execute-operation',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -619,9 +938,30 @@ class BioyondV1RPC(BaseRequest):
return 0
return response.get("code", 0)
def reset_devices(self) -> int:
"""复位设备集合
返回值:
int: 成功返回1失败返回0
"""
response = self.post(
url=f'{self.host}/api/lims/device/reset-devices',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
# ==================== 调度器相关接口 ====================
def scheduler_status(self) -> dict:
"""查询调度器状态
返回值:
dict: 包含 schedulerStatus/hasTask/creationTime 等
"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-status',
params={
@@ -634,7 +974,7 @@ class BioyondV1RPC(BaseRequest):
return response.get("data", {})
def scheduler_start(self) -> int:
"""描述:启动调度器"""
"""启动调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/start',
params={
@@ -647,9 +987,22 @@ class BioyondV1RPC(BaseRequest):
return response.get("code", 0)
def scheduler_pause(self) -> int:
"""描述:暂停调度器"""
"""暂停调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-pause',
url=f'{self.host}/api/lims/scheduler/pause',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
def scheduler_smart_pause(self) -> int:
"""智能暂停调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/smart-pause',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -660,8 +1013,9 @@ class BioyondV1RPC(BaseRequest):
return response.get("code", 0)
def scheduler_continue(self) -> int:
"""继续调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-continue',
url=f'{self.host}/api/lims/scheduler/continue',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -672,9 +1026,9 @@ class BioyondV1RPC(BaseRequest):
return response.get("code", 0)
def scheduler_stop(self) -> int:
"""描述:停止调度器"""
"""停止调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-stop',
url=f'{self.host}/api/lims/scheduler/stop',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -685,9 +1039,9 @@ class BioyondV1RPC(BaseRequest):
return response.get("code", 0)
def scheduler_reset(self) -> int:
"""描述:重置调度器"""
"""复位调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-reset',
url=f'{self.host}/api/lims/scheduler/reset',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -697,6 +1051,26 @@ class BioyondV1RPC(BaseRequest):
return 0
return response.get("code", 0)
def scheduler_reply_error_handling(self, data: Dict[str, Any]) -> int:
"""调度错误处理回复
参数:
data: 错误处理参数
返回值:
int: 成功返回1失败返回0
"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/reply-error-handling',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": data,
})
if not response or response['code'] != 1:
return 0
return response.get("code", 0)
# ==================== 辅助方法 ====================
def _load_material_cache(self):
@@ -705,7 +1079,7 @@ class BioyondV1RPC(BaseRequest):
print("正在加载材料列表缓存...")
# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
material_types = [1, 2]
material_types = [0, 1, 2]
for type_mode in material_types:
print(f"正在加载类型 {type_mode} 的材料...")
@@ -760,3 +1134,23 @@ class BioyondV1RPC(BaseRequest):
def get_available_materials(self):
"""获取所有可用的材料名称列表"""
return list(self.material_cache.keys())
def get_scheduler_state(self) -> Optional[MachineState]:
"""将调度状态字符串映射为枚举值
返回值:
Optional[MachineState]: 映射后的枚举,失败返回 None
"""
data = self.scheduler_status()
if not isinstance(data, dict):
return None
status = data.get("schedulerStatus")
mapping = {
"Init": MachineState.INITIAL,
"Stop": MachineState.STOPPED,
"Running": MachineState.RUNNING,
"Pause": MachineState.PAUSED,
"ErrorPause": MachineState.ERROR_PAUSED,
"ErrorStop": MachineState.ERROR_STOPPED,
}
return mapping.get(status)

View File

@@ -2,330 +2,141 @@
"""
配置文件 - 包含所有配置信息和映射关系
"""
import os
# ==================== API 基础配置 ====================
# BioyondCellWorkstation 默认配置(包含所有必需参数)
# API配置
API_CONFIG = {
# API 连接配置
# "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.1.143:44389"),#实机
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.11.219:44388"),# 仿真机
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")),
"api_key": "",
"api_host": ""
}
# 报送配置
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
# 工作流映射配置
WORKFLOW_MAPPINGS = {
"reactor_taken_out": "",
"reactor_taken_in": "",
"Solid_feeding_vials": "",
"Liquid_feeding_vials(non-titration)": "",
"Liquid_feeding_solvents": "",
"Liquid_feeding(titration)": "",
"liquid_feeding_beaker": "",
"Drip_back": "",
}
# HTTP 服务配置
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.16.11.6"), # HTTP服务监听地址监听计算机飞连ip地址
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
"debug_mode": False,# 调试模式
# 工作流名称到DisplaySectionName的映射
WORKFLOW_TO_SECTION_MAP = {
'reactor_taken_in': '反应器放入',
'liquid_feeding_beaker': '液体投料-烧杯',
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
'Liquid_feeding_solvents': '液体投料-溶剂',
'Solid_feeding_vials': '固体投料-小瓶',
'Liquid_feeding(titration)': '液体投料-滴定',
'reactor_taken_out': '反应器取出'
}
# 库位映射配置
WAREHOUSE_MAPPING = {
"粉末加样头堆栈": {
"粉末堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19da56-1379-ff7c-1745-07e200b44ce2",
"B01": "3a19da56-1379-2424-d751-fe6e94cef938",
"C01": "3a19da56-1379-271c-03e3-6bdb590e395e",
"D01": "3a19da56-1379-277f-2b1b-0d11f7cf92c6",
"E01": "3a19da56-1379-2f1c-a15b-e01db90eb39a",
"F01": "3a19da56-1379-3fa1-846b-088158ac0b3d",
"G01": "3a19da56-1379-5aeb-d0cd-d3b4609d66e1",
"H01": "3a19da56-1379-6077-8258-bdc036870b78",
"I01": "3a19da56-1379-863b-a120-f606baf04617",
"J01": "3a19da56-1379-8a74-74e5-35a9b41d4fd5",
"K01": "3a19da56-1379-b270-b7af-f18773918abe",
"L01": "3a19da56-1379-ba54-6d78-fd770a671ffc",
"M01": "3a19da56-1379-c22d-c96f-0ceb5eb54a04",
"N01": "3a19da56-1379-d64e-c6c5-c72ea4829888",
"O01": "3a19da56-1379-d887-1a3c-6f9cce90f90e",
"P01": "3a19da56-1379-e77d-0e65-7463b238a3b9",
"Q01": "3a19da56-1379-edf6-1472-802ddb628774",
"R01": "3a19da56-1379-f281-0273-e0ef78f0fd97",
"S01": "3a19da56-1379-f924-7f68-df1fa51489f4",
"T01": "3a19da56-1379-ff7c-1745-07e200b44ce2"
# 样品板
"A1": "3a14198e-6929-31f0-8a22-0f98f72260df",
"A2": "3a14198e-6929-4379-affa-9a2935c17f99",
"A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
"A4": "3a14198e-6929-5e99-2b79-80720f7cfb54",
"B1": "3a14198e-6929-f525-9a1b-1857552b28ee",
"B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
"B3": "3a14198e-6929-2d86-a468-602175a2b5aa",
"B4": "3a14198e-6929-1a98-ae57-e97660c489ad",
# 分装板
"C1": "3a14198e-6929-46fe-841e-03dd753f1e4a",
"C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
"C3": "3a14198e-6929-72ac-32ce-9b50245682b8",
"C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
"D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
"D2": "3a14198e-6929-dde1-fc78-34a84b71afdf",
"D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
"D4": "3a14198e-6929-7ac8-915a-fea51cb2e884"
}
},
"配液站内试剂仓库": {
"溶液堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19da43-57b5-294f-d663-154a1cc32270",
"B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2",
"C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f",
"A02": "3a19da43-57b5-8441-db94-b4d3875a4b6c",
"B02": "3a19da43-57b5-3e41-c181-5119dddaf50c",
"C02": "3a19da43-57b5-269b-282d-fba61fe8ce96",
"A03": "3a19da43-57b5-7c1e-d02e-c40e8c33f8a1",
"B03": "3a19da43-57b5-659f-621f-1dcf3f640363",
"C03": "3a19da43-57b5-855a-6e71-f398e376dee1",
"A1": "3a14198e-d724-e036-afdc-2ae39a7f3383",
"A2": "3a14198e-d724-afa4-fc82-0ac8a9016791",
"A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
"A4": "3a14198e-d724-df6d-5e32-5483b3cab583",
"B1": "3a14198e-d724-d818-6d4f-5725191a24b5",
"B2": "3a14198e-d724-be8a-5e0b-012675e195c6",
"B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
"B4": "3a14198e-d724-1e28-c885-574c3df468d0",
"C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
"C2": "3a14198e-d724-ab4e-48cb-817c3c146707",
"C3": "3a14198e-d724-7f18-1853-39d0c62e1d33",
"C4": "3a14198e-d724-28a2-a760-baa896f46b66",
"D1": "3a14198e-d724-d378-d266-2508a224a19f",
"D2": "3a14198e-d724-f56e-468b-0110a8feb36a",
"D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
"D4": "3a14198e-d724-0ddd-9654-f9352a421de9"
}
},
"试剂替换仓库": {
"试剂堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19da51-8f4e-30f3-ea08-4f8498e9b097",
"B01": "3a19da51-8f4e-1da7-beb0-80a4a01e67a8",
"C01": "3a19da51-8f4e-337d-2675-bfac46880b06",
"D01": "3a19da51-8f4e-e514-b92c-9c44dc5e489d",
"E01": "3a19da51-8f4e-22d1-dd5b-9774ddc80402",
"F01": "3a19da51-8f4e-273a-4871-dff41c29bfd9",
"G01": "3a19da51-8f4e-b32f-454f-74bc1a665653",
"H01": "3a19da51-8f4e-8c93-68c9-0b4382320f59",
"I01": "3a19da51-8f4e-360c-0149-291b47c6089b",
"J01": "3a19da51-8f4e-4152-9bca-8d64df8c1af0"
}
},
"自动堆栈-左": {
"uuid": "",
"site_uuids": {
"A01": "3a19debc-84b5-4c1c-d3a1-26830cf273ff",
"A02": "3a19debc-84b5-033b-b31f-6b87f7c2bf52",
"B01": "3a19debc-84b5-3924-172f-719ab01b125c",
"B02": "3a19debc-84b5-aad8-70c6-b8c6bb2d8750"
}
},
"自动堆栈-右": {
"uuid": "",
"site_uuids": {
"A01": "3a19debe-5200-7df2-1dd9-7d202f158864",
"A02": "3a19debe-5200-573b-6120-8b51f50e1e50",
"B01": "3a19debe-5200-7cd8-7666-851b0a97e309",
"B02": "3a19debe-5200-e6d3-96a3-baa6e3d5e484"
}
},
"手动堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe",
"A03": "3a19deae-2c7a-5876-c454-6b7e224ca927",
"B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1",
"B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3",
"B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a",
"C01": "3a19deae-2c7a-32bc-768e-556647e292f3",
"C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4",
"C03": "3a19deae-2c7a-3056-6504-10dc73fbc276",
"D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440",
"D02": "3a19deae-2c7a-61be-601c-b6fb5610499a",
"D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560",
"E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363",
"E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910",
"E03": "3a19deae-2c7a-b163-2219-23df15200311",
"F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a",
"F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679",
"F03": "3a19deae-2c7a-f7c4-12bd-425799425698",
"G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955",
"G02": "3a19deae-2c7a-204e-95ed-1f1950f28343",
"G03": "3a19deae-2c7a-392b-62f1-4907c66343f8",
"H01": "3a19deae-2c7a-5602-e876-d27aca4e3201",
"H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702",
"H03": "3a19deae-2c7a-780b-8965-2e1345f7e834",
"I01": "3a19deae-2c7a-8849-e172-07de14ede928",
"I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0",
"I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4",
"J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6",
"J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205",
"J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9"
}
},
"4号手套箱内部堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa20-a7b1-c665-8b9c-d8099d07d2f6",
"A02": "3a1baa20-a7b1-93a7-c988-f9c8ad6c58c9",
"A03": "3a1baa20-a7b1-00ee-f751-da9b20b6c464",
"A04": "3a1baa20-a7b1-4712-c37b-0b5b658ef7b9",
"B01": "3a1baa20-a7b1-9847-fc9c-96d604cd1a8e",
"B02": "3a1baa20-a7b1-4ae9-e604-0601db06249c",
"B03": "3a1baa20-a7b1-8329-ea75-81ca559d9ce1",
"B04": "3a1baa20-a7b1-89c5-d96f-36e98a8f7268",
"C01": "3a1baa20-a7b1-32ec-39e6-8044733839d6",
"C02": "3a1baa20-a7b1-b573-e426-4c86040348b2",
"C03": "3a1baa20-a7b1-cca7-781e-0522b729bf5d",
"C04": "3a1baa20-a7b1-7c98-5fd9-5855355ae4b3"
}
},
"大分液瓶堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19da3d-4f3d-bcac-2932-7542041e10e0",
"A02": "3a19da3d-4f3d-4d75-38ac-fb58ad0687c3",
"A03": "3a19da3d-4f3d-b25e-f2b1-85342a5b7eae",
"B01": "3a19da3d-4f3d-fd3e-058a-2733a0925767",
"B02": "3a19da3d-4f3d-37bd-a944-c391ad56857f",
"B03": "3a19da3d-4f3d-e353-7862-c6d1d4bc667f",
"C01": "3a19da3d-4f3d-9519-5da7-76179c958e70",
"C02": "3a19da3d-4f3d-b586-d7ed-9ec244f6f937",
"C03": "3a19da3d-4f3d-5061-249b-35dfef732811"
}
},
"小分液瓶堆栈": {
"uuid": "",
"site_uuids": {
"C03": "3a19da40-55bf-8943-d20d-a8b3ea0d16c0"
}
},
"站内Tip头盒堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19deab-d5cc-be1e-5c37-4e9e5a664388",
"A02": "3a19deab-d5cc-b394-8141-27cb3853e8ea",
"B01": "3a19deab-d5cc-4dca-596e-ca7414d5f501",
"B02": "3a19deab-d5cc-9bc0-442b-12d9d59aa62a",
"C01": "3a19deab-d5cc-2eaf-b6a4-f0d54e4f1246",
"C02": "3a19deab-d5cc-d9f4-25df-b8018c372bc7"
}
},
"配液站内配液大板仓库(无需提前上料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a21dc-06af-3915-9cb9-80a9dc42f386"
}
},
"配液站内配液小板仓库(无需以前入料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a21de-8e8b-7938-2d06-858b36c10e31"
}
},
"移液站内大瓶板仓库(无需提前如料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a224c-c727-fa62-1f2b-0037a84b9fca"
}
},
"移液站内小瓶板仓库(无需提前入料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a224d-ed49-710c-a9c3-3fc61d479cbb"
}
},
"适配器位仓库": {
"uuid": "",
"site_uuids": {
"A01": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c"
}
},
"1号2号手套箱交接堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa49-7f77-35aa-60b1-e55a45d065fa"
}
},
"2号手套箱内部堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa4b-393e-9f86-3921-7a18b0a8e371",
"A02": "3a1baa4b-393e-9425-928b-ee0f6f679d44",
"A03": "3a1baa4b-393e-0baf-632b-59dfdc931a3a",
"B01": "3a1baa4b-393e-f8aa-c8a9-956f3132f05c",
"B02": "3a1baa4b-393e-ef05-42f6-53f4c6e89d70",
"B03": "3a1baa4b-393e-c07b-a924-a9f0dfda9711",
"C01": "3a1baa4b-393e-4c2b-821a-16a7fe025c48",
"C02": "3a1baa4b-393e-2eaf-61a1-9063c832d5a2",
"C03": "3a1baa4b-393e-034e-8e28-8626d934a85f"
"A1": "3a14198c-c2cf-8b40-af28-b467808f1c36",
"A2": "3a14198c-c2d0-f3e7-871a-e470d144296f",
"A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094",
"A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
"B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8",
"B2": "3a14198c-c2d0-1559-105d-0ea30682cab4",
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
}
}
}
# 物料类型配置
MATERIAL_TYPE_MAPPINGS = {
"100ml液体": ("YB_100ml_yeti", "d37166b3-ecaa-481e-bd84-3032b795ba07"),
"": ("YB_ye", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
"高粘液": ("YB_gaonianye", "abe8df30-563d-43d2-85e0-cabec59ddc16"),
"加样头(大)": ("YB_jia_yang_tou_da_Carrier", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "加样头(大)板": ("YB_jia_yang_tou_da", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"),
"5ml分液瓶板": ("YB_5ml_fenyepingban", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"),
"5ml分液": ("YB_5ml_fenyeping", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"),
"20ml分液瓶板": ("YB_20ml_fenyepingban", "3a192fa4-47db-3449-162a-eaf8aba57e27"),
"20ml分液瓶": ("YB_20ml_fenyeping", "3a192c2b-19e8-f0a3-035e-041ca8ca1035"),
"配液瓶(小)板": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
"配液瓶(小)": ("YB_pei_ye_xiao_Bottle", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
"配液瓶(大)板": ("YB_peiyepingdaban", "53e50377-32dc-4781-b3c0-5ce45bc7dc27"),
"配液瓶(大)": ("YB_pei_ye_da_Bottle", "19c52ad1-51c5-494f-8854-576f4ca9c6ca"),
"适配器块": ("YB_shi_pei_qi_kuai", "efc3bb32-d504-4890-91c0-b64ed3ac80cf"),
"枪头盒": ("YB_qiang_tou_he", "3a192c2e-20f3-a44a-0334-c8301839d0b3"),
"枪头": ("YB_qiang_tou", "b6196971-1050-46da-9927-333e8dea062d"),
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
"10%分装小": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
}
SOLID_LIQUID_MAPPINGS = {
# 固体
"LiDFOB": {
"typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
"code": "",
"barCode": "",
"name": "LiDFOB",
"unit": "g",
"parameters": "",
"quantity": "2",
"warningQuantity": "1",
"details": []
# 步骤参数配置各工作流的步骤UUID
WORKFLOW_STEP_IDS = {
"reactor_taken_in": {
"config": ""
},
# "LiPF6": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiPF6",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# "LiFSI": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiFSI",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# "DTC": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "DTC",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# "LiPO2F2": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiPO2F2",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# 液体
# "SA": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "VC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "AND": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "HTCN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DENE": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "TMSP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "TMSB": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "SN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "FEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
"liquid_feeding_beaker": {
"liquid": "",
"observe": ""
},
"liquid_feeding_vials_non_titration": {
"liquid": "",
"observe": ""
},
"liquid_feeding_solvents": {
"liquid": "",
"observe": ""
},
"solid_feeding_vials": {
"feeding": "",
"observe": ""
},
"liquid_feeding_titration": {
"liquid": "",
"observe": ""
},
"drip_back": {
"liquid": "",
"observe": ""
}
}
WORKFLOW_MAPPINGS = {}
LOCATION_MAPPING = {}
ACTION_NAMES = {}
HTTP_SERVICE_CONFIG = {}

View File

@@ -1,8 +1,25 @@
from datetime import datetime
import json
import time
from typing import Optional, Dict, Any, List
from typing_extensions import TypedDict
import requests
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
import json
import sys
from pathlib import Path
import importlib
class ComputeExperimentDesignReturn(TypedDict):
solutions: list
titration: dict
solvents: dict
feeding_order: list
return_info: str
class BioyondDispensingStation(BioyondWorkstation):
@@ -23,6 +40,111 @@ class BioyondDispensingStation(BioyondWorkstation):
# self._logger = SimpleLogger()
# self.is_running = False
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
self.order_completion_status = {}
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
"""项目接口通用POST调用
参数:
endpoint: 接口路径(例如 /api/lims/order/brief-step-paramerers
data: 请求体中的 data 字段内容
返回:
dict: 服务端响应,失败时返回 {code:0,message,...}
"""
request_data = {
"apiKey": API_CONFIG["api_key"],
"requestTime": self.hardware_interface.get_current_time_iso8601(),
"data": data
}
try:
response = requests.post(
f"{self.hardware_interface.host}{endpoint}",
json=request_data,
headers={"Content-Type": "application/json"},
timeout=30
)
result = response.json()
return result if isinstance(result, dict) else {"code": 0, "message": "非JSON响应"}
except json.JSONDecodeError:
return {"code": 0, "message": "非JSON响应"}
except requests.exceptions.Timeout:
return {"code": 0, "message": "请求超时"}
except requests.exceptions.RequestException as e:
return {"code": 0, "message": str(e)}
def _delete_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
"""项目接口通用DELETE调用
参数:
endpoint: 接口路径(例如 /api/lims/order/workflows
data: 请求体中的 data 字段内容
返回:
dict: 服务端响应,失败时返回 {code:0,message,...}
"""
request_data = {
"apiKey": API_CONFIG["api_key"],
"requestTime": self.hardware_interface.get_current_time_iso8601(),
"data": data
}
try:
response = requests.delete(
f"{self.hardware_interface.host}{endpoint}",
json=request_data,
headers={"Content-Type": "application/json"},
timeout=30
)
result = response.json()
return result if isinstance(result, dict) else {"code": 0, "message": "非JSON响应"}
except json.JSONDecodeError:
return {"code": 0, "message": "非JSON响应"}
except requests.exceptions.Timeout:
return {"code": 0, "message": "请求超时"}
except requests.exceptions.RequestException as e:
return {"code": 0, "message": str(e)}
def compute_experiment_design(
self,
ratio: dict,
wt_percent: str = "0.25",
m_tot: str = "70",
titration_percent: str = "0.03",
) -> ComputeExperimentDesignReturn:
try:
if isinstance(ratio, str):
try:
ratio = json.loads(ratio)
except Exception:
ratio = {}
root = str(Path(__file__).resolve().parents[3])
if root not in sys.path:
sys.path.append(root)
try:
mod = importlib.import_module("tem.compute")
except Exception as e:
raise BioyondException(f"无法导入计算模块: {e}")
try:
wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent
mt = float(m_tot) if isinstance(m_tot, str) else m_tot
tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent
except Exception as e:
raise BioyondException(f"参数解析失败: {e}")
res = mod.generate_experiment_design(ratio=ratio, wt_percent=wp, m_tot=mt, titration_percent=tp)
out = {
"solutions": res.get("solutions", []),
"titration": res.get("titration", {}),
"solvents": res.get("solvents", {}),
"feeding_order": res.get("feeding_order", []),
"return_info": json.dumps(res, ensure_ascii=False)
}
return out
except BioyondException:
raise
except Exception as e:
raise BioyondException(str(e))
# 90%10%小瓶投料任务创建方法
def create_90_10_vial_feeding_task(self,
order_name: str = None,
@@ -270,7 +392,45 @@ class BioyondDispensingStation(BioyondWorkstation):
# 7. 调用create_order方法创建任务
result = self.hardware_interface.create_order(json_str)
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
return json.dumps({"suc": True})
# 8. 解析结果获取order_id
order_id = None
if isinstance(result, str):
# result 格式: "{'3a1d895c-4d39-d504-1398-18f5a40bac1e': [{'id': '...', ...}]}"
# 第一个键就是order_id (UUID)
try:
# 尝试解析字符串为字典
import ast
result_dict = ast.literal_eval(result)
# 获取第一个键作为order_id
if result_dict and isinstance(result_dict, dict):
first_key = list(result_dict.keys())[0]
order_id = first_key
self.hardware_interface._logger.info(f"✓ 成功提取order_id: {order_id}")
else:
self.hardware_interface._logger.warning(f"result_dict格式异常: {result_dict}")
except Exception as e:
self.hardware_interface._logger.error(f"✗ 无法从结果中提取order_id: {e}, result类型={type(result)}")
elif isinstance(result, dict):
# 如果已经是字典
if result:
first_key = list(result.keys())[0]
order_id = first_key
self.hardware_interface._logger.info(f"✓ 成功提取order_id(dict): {order_id}")
if not order_id:
self.hardware_interface._logger.warning(
f"⚠ 未能提取order_idresult={result[:100] if isinstance(result, str) else result}"
)
# 返回成功结果和构建的JSON数据
return json.dumps({
"suc": True,
"order_code": order_code,
"order_id": order_id,
"result": result,
"order_params": order_data
})
except BioyondException:
# 重新抛出BioyondException
@@ -398,7 +558,37 @@ class BioyondDispensingStation(BioyondWorkstation):
result = self.hardware_interface.create_order(json_str)
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
return json.dumps({"suc": True})
# 8. 解析结果获取order_id
order_id = None
if isinstance(result, str):
try:
import ast
result_dict = ast.literal_eval(result)
if result_dict and isinstance(result_dict, dict):
first_key = list(result_dict.keys())[0]
order_id = first_key
self.hardware_interface._logger.info(f"✓ 成功提取order_id: {order_id}")
else:
self.hardware_interface._logger.warning(f"result_dict格式异常: {result_dict}")
except Exception as e:
self.hardware_interface._logger.error(f"✗ 无法从结果中提取order_id: {e}")
elif isinstance(result, dict):
if result:
first_key = list(result.keys())[0]
order_id = first_key
self.hardware_interface._logger.info(f"✓ 成功提取order_id(dict): {order_id}")
if not order_id:
self.hardware_interface._logger.warning(f"⚠ 未能提取order_id")
# 返回成功结果和构建的JSON数据
return json.dumps({
"suc": True,
"order_code": order_code,
"order_id": order_id,
"result": result,
"order_params": order_data
})
except BioyondException:
# 重新抛出BioyondException
@@ -499,15 +689,24 @@ class BioyondDispensingStation(BioyondWorkstation):
hold_m_name=hold_m_name
)
# 解析返回结果以获取order_code和order_id
result_data = json.loads(result) if isinstance(result, str) else result
order_code = result_data.get("order_code")
order_id = result_data.get("order_id")
order_params = result_data.get("order_params", {})
results.append({
"index": idx + 1,
"name": name,
"success": True,
"hold_m_name": hold_m_name
"order_code": order_code,
"order_id": order_id,
"hold_m_name": hold_m_name,
"order_params": order_params
})
success_count += 1
self.hardware_interface._logger.info(
f"成功创建二胺溶液配置任务: {name}"
f"成功创建二胺溶液配置任务: {name}, order_code={order_code}, order_id={order_id}"
)
except BioyondException as e:
@@ -533,11 +732,17 @@ class BioyondDispensingStation(BioyondWorkstation):
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
)
# 提取所有成功任务的order_code和order_id
order_codes = [r["order_code"] for r in results if r["success"]]
order_ids = [r["order_id"] for r in results if r["success"]]
# 返回汇总结果
summary = {
"total": len(solutions),
"success": success_count,
"failed": failed_count,
"order_codes": order_codes,
"order_ids": order_ids,
"details": results
}
@@ -546,8 +751,13 @@ class BioyondDispensingStation(BioyondWorkstation):
f"成功={success_count}, 失败={failed_count}"
)
# 返回JSON字符串格式
return json.dumps(summary, ensure_ascii=False)
# 构建返回结果
summary["return_info"] = {
"order_codes": order_codes,
"order_ids": order_ids,
}
return summary
except BioyondException:
raise
@@ -556,6 +766,40 @@ class BioyondDispensingStation(BioyondWorkstation):
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
def brief_step_parameters(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""获取简要步骤参数(站点项目接口)
参数:
data: 查询参数字典
返回值:
dict: 接口返回数据
"""
return self._post_project_api("/api/lims/order/brief-step-paramerers", data)
def project_order_report(self, order_id: str) -> Dict[str, Any]:
"""查询项目端订单报告(兼容旧路径)
参数:
order_id: 订单ID
返回值:
dict: 报告数据
"""
return self._post_project_api("/api/lims/order/project-order-report", order_id)
def workflow_sample_locations(self, workflow_id: str) -> Dict[str, Any]:
"""查询工作流样品库位(站点项目接口)
参数:
workflow_id: 工作流ID
返回值:
dict: 位置信息数据
"""
return self._post_project_api("/api/lims/storage/workflow-sample-locations", workflow_id)
# 批量创建90%10%小瓶投料任务
def batch_create_90_10_vial_feeding_tasks(self,
titration,
@@ -613,22 +857,15 @@ class BioyondDispensingStation(BioyondWorkstation):
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
raise BioyondException("titration 数据缺少必要参数")
# 将main_portion平均分成3份作为90%物料3个小瓶
portion_90 = main_portion / 3
# 调用单个任务创建方法
result = self.create_90_10_vial_feeding_task(
order_name=f"90%10%小瓶投料-{name}",
speed=speed,
temperature=temperature,
delay_time=delay_time,
# 90%物料 - 主称固体平均分成3份
# 90%物料 - 主称固体直接使用main_portion
percent_90_1_assign_material_name=name,
percent_90_1_target_weigh=str(round(portion_90, 6)),
percent_90_2_assign_material_name=name,
percent_90_2_target_weigh=str(round(portion_90, 6)),
percent_90_3_assign_material_name=name,
percent_90_3_target_weigh=str(round(portion_90, 6)),
percent_90_1_target_weigh=str(round(main_portion, 6)),
# 10%物料 - 滴定固体 + 滴定溶剂只使用第1个10%小瓶)
percent_10_1_assign_material_name=name,
percent_10_1_target_weigh=str(round(titration_portion, 6)),
@@ -637,29 +874,54 @@ class BioyondDispensingStation(BioyondWorkstation):
hold_m_name=hold_m_name
)
summary = {
# 解析返回结果以获取order_code和order_id
result_data = json.loads(result) if isinstance(result, str) else result
order_code = result_data.get("order_code")
order_id = result_data.get("order_id")
order_params = result_data.get("order_params", {})
# 构建详细信息(保持原有结构)
detail = {
"index": 1,
"name": name,
"success": True,
"order_code": order_code,
"order_id": order_id,
"hold_m_name": hold_m_name,
"material_name": name,
"90_vials": {
"count": 3,
"weight_per_vial": round(portion_90, 6),
"count": 1,
"weight_per_vial": round(main_portion, 6),
"total_weight": round(main_portion, 6)
},
"10_vials": {
"count": 1,
"solid_weight": round(titration_portion, 6),
"liquid_volume": round(titration_solvent, 6)
}
},
"order_params": order_params
}
# 构建批量结果格式与diamine_solution_tasks保持一致
summary = {
"total": 1,
"success": 1,
"failed": 0,
"order_codes": [order_code],
"order_ids": [order_id],
"details": [detail]
}
self.hardware_interface._logger.info(
f"成功创建90%10%小瓶投料任务: {hold_m_name}, "
f"90%物料={portion_90:.6f}g×3, 10%物料={titration_portion:.6f}g+{titration_solvent:.6f}mL"
f"成功创建90%10%小瓶投料任务: {name}, order_code={order_code}, order_id={order_id}"
)
# 返回JSON字符串格式
return json.dumps(summary, ensure_ascii=False)
# 构建返回结果
summary["return_info"] = {
"order_codes": [order_code],
"order_ids": [order_id],
}
return summary
except BioyondException:
raise
@@ -668,6 +930,571 @@ class BioyondDispensingStation(BioyondWorkstation):
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
def _extract_actuals_from_report(self, report) -> Dict[str, Any]:
data = report.get('data') if isinstance(report, dict) else None
actual_target_weigh = None
actual_volume = None
if data:
extra = data.get('extraProperties') or {}
if isinstance(extra, dict):
for v in extra.values():
obj = None
try:
obj = json.loads(v) if isinstance(v, str) else v
except Exception:
obj = None
if isinstance(obj, dict):
tw = obj.get('targetWeigh')
vol = obj.get('volume')
if tw is not None:
try:
actual_target_weigh = float(tw)
except Exception:
pass
if vol is not None:
try:
actual_volume = float(vol)
except Exception:
pass
return {
'actualTargetWeigh': actual_target_weigh,
'actualVolume': actual_volume
}
# 等待多个任务完成并获取实验报告
def wait_for_multiple_orders_and_get_reports(self,
batch_create_result: str = None,
timeout: int = 7200,
check_interval: int = 10) -> Dict[str, Any]:
"""
同时等待多个任务完成并获取实验报告
参数说明:
- batch_create_result: 批量创建任务的返回结果JSON字符串包含order_codes和order_ids数组
- timeout: 超时时间默认7200秒2小时
- check_interval: 检查间隔默认10秒
返回: 包含所有任务状态和报告的字典
{
"total": 2,
"completed": 2,
"timeout": 0,
"elapsed_time": 120.5,
"reports": [
{
"order_code": "task_vial_1",
"order_id": "uuid1",
"status": "completed",
"completion_status": 30,
"report": {...}
},
...
]
}
异常:
- BioyondException: 所有任务都超时或发生错误
"""
try:
# 参数类型转换
timeout = int(timeout) if timeout else 7200
check_interval = int(check_interval) if check_interval else 10
# 验证batch_create_result参数
if not batch_create_result or batch_create_result == "":
raise BioyondException("batch_create_result参数为空请确保从batch_create节点正确连接handle")
# 解析batch_create_result JSON对象
try:
# 清理可能存在的截断标记 [...]
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
batch_create_result = batch_create_result.replace('[...]', '[]')
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
# 兼容外层包装格式 {error, suc, return_value}
if isinstance(result_obj, dict) and "return_value" in result_obj:
inner = result_obj.get("return_value")
if isinstance(inner, str):
result_obj = json.loads(inner)
elif isinstance(inner, dict):
result_obj = inner
# 从summary对象中提取order_codes和order_ids
order_codes = result_obj.get("order_codes", [])
order_ids = result_obj.get("order_ids", [])
except json.JSONDecodeError as e:
raise BioyondException(f"解析batch_create_result失败: {e}")
except Exception as e:
raise BioyondException(f"处理batch_create_result时出错: {e}")
# 验证提取的数据
if not order_codes:
raise BioyondException("batch_create_result中未找到order_codes字段或为空")
if not order_ids:
raise BioyondException("batch_create_result中未找到order_ids字段或为空")
# 确保order_codes和order_ids是列表类型
if not isinstance(order_codes, list):
order_codes = [order_codes] if order_codes else []
if not isinstance(order_ids, list):
order_ids = [order_ids] if order_ids else []
codes_list = order_codes
ids_list = order_ids
if len(codes_list) != len(ids_list):
raise BioyondException(
f"order_codes数量({len(codes_list)})与order_ids数量({len(ids_list)})不匹配"
)
if not codes_list or not ids_list:
raise BioyondException("order_codes和order_ids不能为空")
# 初始化跟踪变量
total = len(codes_list)
pending_orders = {code: {"order_id": ids_list[i], "completed": False}
for i, code in enumerate(codes_list)}
reports = []
start_time = time.time()
self.hardware_interface._logger.info(
f"开始等待 {total} 个任务完成: {', '.join(codes_list)}"
)
# 轮询检查任务状态
while pending_orders:
elapsed_time = time.time() - start_time
# 检查超时
if elapsed_time > timeout:
# 收集超时任务
timeout_orders = list(pending_orders.keys())
self.hardware_interface._logger.error(
f"等待任务完成超时,剩余未完成任务: {', '.join(timeout_orders)}"
)
# 为超时任务添加记录
for order_code in timeout_orders:
reports.append({
"order_code": order_code,
"order_id": pending_orders[order_code]["order_id"],
"status": "timeout",
"completion_status": None,
"report": None,
"extracted": None,
"elapsed_time": elapsed_time
})
break
# 检查每个待完成的任务
completed_in_this_round = []
for order_code in list(pending_orders.keys()):
order_id = pending_orders[order_code]["order_id"]
# 检查任务是否完成
if order_code in self.order_completion_status:
completion_info = self.order_completion_status[order_code]
self.hardware_interface._logger.info(
f"检测到任务 {order_code} 已完成,状态: {completion_info.get('status')}"
)
# 获取实验报告
try:
report = self.project_order_report(order_id)
if not report:
self.hardware_interface._logger.warning(
f"任务 {order_code} 已完成但无法获取报告"
)
report = {"error": "无法获取报告"}
else:
self.hardware_interface._logger.info(
f"成功获取任务 {order_code} 的实验报告"
)
reports.append({
"order_code": order_code,
"order_id": order_id,
"status": "completed",
"completion_status": completion_info.get('status'),
"report": report,
"extracted": self._extract_actuals_from_report(report),
"elapsed_time": elapsed_time
})
# 标记为已完成
completed_in_this_round.append(order_code)
# 清理完成状态记录
del self.order_completion_status[order_code]
except Exception as e:
self.hardware_interface._logger.error(
f"查询任务 {order_code} 报告失败: {str(e)}"
)
reports.append({
"order_code": order_code,
"order_id": order_id,
"status": "error",
"completion_status": completion_info.get('status'),
"report": None,
"extracted": None,
"error": str(e),
"elapsed_time": elapsed_time
})
completed_in_this_round.append(order_code)
# 从待完成列表中移除已完成的任务
for order_code in completed_in_this_round:
del pending_orders[order_code]
# 如果还有待完成的任务,等待后继续
if pending_orders:
time.sleep(check_interval)
# 每分钟记录一次等待状态
new_elapsed_time = time.time() - start_time
if int(new_elapsed_time) % 60 == 0 and new_elapsed_time > 0:
self.hardware_interface._logger.info(
f"批量等待任务中... 已完成 {len(reports)}/{total}, "
f"待完成: {', '.join(pending_orders.keys())}, "
f"已等待 {int(new_elapsed_time/60)} 分钟"
)
# 统计结果
completed_count = sum(1 for r in reports if r['status'] == 'completed')
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
error_count = sum(1 for r in reports if r['status'] == 'error')
final_elapsed_time = time.time() - start_time
summary = {
"total": total,
"completed": completed_count,
"timeout": timeout_count,
"error": error_count,
"elapsed_time": round(final_elapsed_time, 2),
"reports": reports
}
self.hardware_interface._logger.info(
f"批量等待任务完成: 总数={total}, 成功={completed_count}, "
f"超时={timeout_count}, 错误={error_count}, 耗时={final_elapsed_time:.1f}"
)
# 返回字典格式,在顶层包含统计信息
return {
"return_info": json.dumps(summary, ensure_ascii=False)
}
except BioyondException:
raise
except Exception as e:
error_msg = f"批量等待任务完成时发生未预期的错误: {str(e)}"
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
def process_order_finish_report(self, report_request, used_materials) -> Dict[str, Any]:
"""
重写父类方法,处理任务完成报送并记录到 order_completion_status
Args:
report_request: WorkstationReportRequest 对象,包含任务完成信息
used_materials: 物料使用记录列表
Returns:
Dict[str, Any]: 处理结果
"""
try:
# 调用父类方法
result = super().process_order_finish_report(report_request, used_materials)
# 记录任务完成状态
data = report_request.data
order_code = data.get('orderCode')
if order_code:
self.order_completion_status[order_code] = {
'status': data.get('status'),
'order_name': data.get('orderName'),
'timestamp': datetime.now().isoformat(),
'start_time': data.get('startTime'),
'end_time': data.get('endTime')
}
self.hardware_interface._logger.info(
f"已记录任务完成状态: {order_code}, status={data.get('status')}"
)
return result
except Exception as e:
self.hardware_interface._logger.error(f"处理任务完成报送失败: {e}")
return {"processed": False, "error": str(e)}
def transfer_materials_to_reaction_station(
self,
target_device_id: str,
transfer_groups: list
) -> dict:
"""
将配液站完成的物料转移到指定反应站的堆栈库位
支持多组转移任务,每组包含物料名称、目标堆栈和目标库位
Args:
target_device_id: 目标反应站设备ID(所有转移组使用同一个设备)
transfer_groups: 转移任务组列表,每组包含:
- materials: 物料名称(字符串,将通过RPC查询)
- target_stack: 目标堆栈名称(如"堆栈1左")
- target_sites: 目标库位(如"A01")
Returns:
dict: 转移结果
{
"success": bool,
"total_groups": int,
"successful_groups": int,
"failed_groups": int,
"target_device_id": str,
"details": [...]
}
"""
try:
# 验证参数
if not target_device_id:
raise ValueError("目标设备ID不能为空")
if not transfer_groups:
raise ValueError("转移任务组列表不能为空")
if not isinstance(transfer_groups, list):
raise ValueError("transfer_groups必须是列表类型")
# 标准化设备ID格式: 确保以 /devices/ 开头
if not target_device_id.startswith("/devices/"):
if target_device_id.startswith("/"):
target_device_id = f"/devices{target_device_id}"
else:
target_device_id = f"/devices/{target_device_id}"
self.hardware_interface._logger.info(
f"目标设备ID标准化为: {target_device_id}"
)
self.hardware_interface._logger.info(
f"开始执行批量物料转移: {len(transfer_groups)}组任务 -> {target_device_id}"
)
from .config import WAREHOUSE_MAPPING
results = []
successful_count = 0
failed_count = 0
for idx, group in enumerate(transfer_groups, 1):
try:
# 提取参数
material_name = group.get("materials", "")
target_stack = group.get("target_stack", "")
target_sites = group.get("target_sites", "")
# 验证必填参数
if not material_name:
raise ValueError(f"{idx}组: 物料名称不能为空")
if not target_stack:
raise ValueError(f"{idx}组: 目标堆栈不能为空")
if not target_sites:
raise ValueError(f"{idx}组: 目标库位不能为空")
self.hardware_interface._logger.info(
f"处理第{idx}组转移: {material_name} -> "
f"{target_device_id}/{target_stack}/{target_sites}"
)
# 通过物料名称从deck获取ResourcePLR对象
try:
material_resource = self.deck.get_resource(material_name)
if not material_resource:
raise ValueError(f"在deck中未找到物料: {material_name}")
self.hardware_interface._logger.info(
f"从deck获取到物料 {material_name}: {material_resource}"
)
except Exception as e:
raise ValueError(
f"获取物料 {material_name} 失败: {str(e)}请确认物料已正确加载到deck中"
)
# 验证目标堆栈是否存在
if target_stack not in WAREHOUSE_MAPPING:
raise ValueError(
f"未知的堆栈名称: {target_stack}"
f"可选值: {list(WAREHOUSE_MAPPING.keys())}"
)
# 验证库位是否有效
stack_sites = WAREHOUSE_MAPPING[target_stack].get("site_uuids", {})
if target_sites not in stack_sites:
raise ValueError(
f"库位 {target_sites} 不存在于堆栈 {target_stack} 中,"
f"可选库位: {list(stack_sites.keys())}"
)
# 获取目标库位的UUID
target_site_uuid = stack_sites[target_sites]
if not target_site_uuid:
raise ValueError(
f"库位 {target_sites} 的 UUID 未配置,请在 WAREHOUSE_MAPPING 中完善"
)
# 目标位点包含UUID
future = ROS2DeviceNode.run_async_func(
self._ros_node.get_resource_with_dir,
True,
**{
"resource_id": f"/reaction_station_bioyond/Bioyond_Deck/{target_stack}",
"with_children": True,
},
)
# 等待异步完成后再获取结果
if not future:
raise ValueError(f"获取目标堆栈资源future无效: {target_stack}")
while not future.done():
time.sleep(0.1)
target_site_resource = future.result()
# 调用父类的 transfer_resource_to_another 方法
# 传入ResourcePLR对象和目标位点资源
future = self.transfer_resource_to_another(
resource=[material_resource],
mount_resource=[target_site_resource],
sites=[target_sites],
mount_device_id=target_device_id
)
# 等待异步任务完成(轮询直到完成,再取结果)
if future:
try:
while not future.done():
time.sleep(0.1)
future.result()
self.hardware_interface._logger.info(
f"异步转移任务已完成: {material_name}"
)
except Exception as e:
raise ValueError(f"转移任务执行失败: {str(e)}")
self.hardware_interface._logger.info(
f"{idx}组转移成功: {material_name} -> "
f"{target_device_id}/{target_stack}/{target_sites}"
)
successful_count += 1
results.append({
"group_index": idx,
"success": True,
"material_name": material_name,
"target_stack": target_stack,
"target_site": target_sites,
"message": "转移成功"
})
except Exception as e:
error_msg = f"{idx}组转移失败: {str(e)}"
self.hardware_interface._logger.error(error_msg)
failed_count += 1
results.append({
"group_index": idx,
"success": False,
"material_name": group.get("materials", ""),
"error": str(e)
})
# 返回汇总结果
return {
"success": failed_count == 0,
"total_groups": len(transfer_groups),
"successful_groups": successful_count,
"failed_groups": failed_count,
"target_device_id": target_device_id,
"details": results,
"message": f"完成 {len(transfer_groups)} 组转移任务到 {target_device_id}: "
f"{successful_count} 成功, {failed_count} 失败"
}
except Exception as e:
error_msg = f"批量转移物料失败: {str(e)}"
self.hardware_interface._logger.error(error_msg)
return {
"success": False,
"total_groups": len(transfer_groups) if transfer_groups else 0,
"successful_groups": 0,
"failed_groups": len(transfer_groups) if transfer_groups else 0,
"target_device_id": target_device_id if target_device_id else "",
"error": error_msg
}
def query_resource_by_name(self, material_name: str):
"""
通过物料名称查询资源对象(适用于Bioyond系统)
Args:
material_name: 物料名称
Returns:
物料ID或None
"""
try:
# Bioyond系统使用material_cache存储物料信息
if not hasattr(self.hardware_interface, 'material_cache'):
self.hardware_interface._logger.error(
"hardware_interface没有material_cache属性"
)
return None
material_cache = self.hardware_interface.material_cache
self.hardware_interface._logger.info(
f"查询物料 '{material_name}', 缓存中共有 {len(material_cache)} 个物料"
)
# 调试: 打印前几个物料信息
if material_cache:
cache_items = list(material_cache.items())[:5]
for name, material_id in cache_items:
self.hardware_interface._logger.debug(
f"缓存物料: name={name}, id={material_id}"
)
# 直接从缓存中查找
if material_name in material_cache:
material_id = material_cache[material_name]
self.hardware_interface._logger.info(
f"找到物料: {material_name} -> ID: {material_id}"
)
return material_id
self.hardware_interface._logger.warning(
f"未找到物料: {material_name} (缓存中无此物料)"
)
# 打印所有可用物料名称供参考
available_materials = list(material_cache.keys())
if available_materials:
self.hardware_interface._logger.info(
f"可用物料列表(前10个): {available_materials[:10]}"
)
return None
except Exception as e:
self.hardware_interface._logger.error(
f"查询物料失败 {material_name}: {str(e)}"
)
return None
if __name__ == "__main__":
bioyond = BioyondDispensingStation(config={
@@ -1089,4 +1916,3 @@ if __name__ == "__main__":
# id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc"
# bioyond.sample_waste_removal(id)

View File

@@ -1,7 +1,12 @@
import json
import time
import requests
from typing import List, Dict, Any
from pathlib import Path
from datetime import datetime
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import MachineState
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
from unilabos.devices.workstation.bioyond_studio.config import (
WORKFLOW_STEP_IDS,
WORKFLOW_TO_SECTION_MAP,
@@ -10,6 +15,37 @@ from unilabos.devices.workstation.bioyond_studio.config import (
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
class BioyondReactor:
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
self.in_temperature = 0.0
self.out_temperature = 0.0
self.pt100_temperature = 0.0
self.sensor_average_temperature = 0.0
self.target_temperature = 0.0
self.setting_temperature = 0.0
self.viscosity = 0.0
self.average_viscosity = 0.0
self.speed = 0.0
self.force = 0.0
def update_metrics(self, payload: Dict[str, Any]):
def _f(v):
try:
return float(v)
except Exception:
return 0.0
self.target_temperature = _f(payload.get("targetTemperature"))
self.setting_temperature = _f(payload.get("settingTemperature"))
self.in_temperature = _f(payload.get("inTemperature"))
self.out_temperature = _f(payload.get("outTemperature"))
self.pt100_temperature = _f(payload.get("pt100Temperature"))
self.sensor_average_temperature = _f(payload.get("sensorAverageTemperature"))
self.speed = _f(payload.get("speed"))
self.force = _f(payload.get("force"))
self.viscosity = _f(payload.get("viscosity"))
self.average_viscosity = _f(payload.get("averageViscosity"))
class BioyondReactionStation(BioyondWorkstation):
"""Bioyond反应站类
@@ -37,6 +73,19 @@ class BioyondReactionStation(BioyondWorkstation):
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
self.in_temperature = 0.0
self.out_temperature = 0.0
self.pt100_temperature = 0.0
self.sensor_average_temperature = 0.0
self.target_temperature = 0.0
self.setting_temperature = 0.0
self.viscosity = 0.0
self.average_viscosity = 0.0
self.speed = 0.0
self.force = 0.0
self._frame_to_reactor_id = {1: "reactor_1", 2: "reactor_2", 3: "reactor_3", 4: "reactor_4", 5: "reactor_5"}
# ==================== 工作流方法 ====================
def reactor_taken_out(self):
@@ -232,7 +281,7 @@ class BioyondReactionStation(BioyondWorkstation):
temperature: 温度设定(°C)
"""
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
if volume is None and solvents is not None:
if not volume and solvents is not None:
# 参数类型转换:如果是字符串则解析为字典
if isinstance(solvents, str):
try:
@@ -291,22 +340,39 @@ class BioyondReactionStation(BioyondWorkstation):
def liquid_feeding_titration(
self,
volume_formula: str,
assign_material_name: str,
titration_type: str = "1",
volume_formula: str = None,
x_value: str = None,
feeding_order_data: str = None,
extracted_actuals: str = None,
titration_type: str = "2",
time: str = "90",
torque_variation: int = 2,
temperature: float = 25.00
):
"""液体进料(滴定)
支持两种模式:
1. 直接提供 volume_formula (传统方式)
2. 自动计算公式: 提供 x_value, feeding_order_data, extracted_actuals (新方式)
Args:
volume_formula: 分液公式(μL)
assign_material_name: 物料名称
titration_type: 是否滴定(1=否, 2=是)
volume_formula: 分液公式(μL),如果提供则直接使用,否则自动计算
x_value: 手工输入的x值,格式如 "1-2-3"
feeding_order_data: feeding_order JSON字符串或对象,用于获取m二酐值
extracted_actuals: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh和actualVolume
titration_type: 是否滴定(1=否, 2=是),默认2
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度(°C)
自动公式模板: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
其中:
- m二酐滴定 = actualTargetWeigh (从extracted_actuals获取)
- V二酐滴定 = actualVolume (从extracted_actuals获取)
- x = x_value (手工输入)
- m二酐 = feeding_order中type为"main_anhydride"的amount值
"""
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
@@ -316,6 +382,84 @@ class BioyondReactionStation(BioyondWorkstation):
if isinstance(temperature, str):
temperature = float(temperature)
# 如果没有直接提供volume_formula,则自动计算
if not volume_formula and x_value and feeding_order_data and extracted_actuals:
# 1. 解析 feeding_order_data 获取 m二酐
if isinstance(feeding_order_data, str):
try:
feeding_order_data = json.loads(feeding_order_data)
except json.JSONDecodeError as e:
raise ValueError(f"feeding_order_data JSON解析失败: {str(e)}")
# 支持两种格式:
# 格式1: 直接是数组 [{...}, {...}]
# 格式2: 对象包裹 {"feeding_order": [{...}, {...}]}
if isinstance(feeding_order_data, list):
feeding_order_list = feeding_order_data
elif isinstance(feeding_order_data, dict):
feeding_order_list = feeding_order_data.get("feeding_order", [])
else:
raise ValueError("feeding_order_data 必须是数组或包含feeding_order的字典")
# 从feeding_order中找到main_anhydride的amount
m_anhydride = None
for item in feeding_order_list:
if item.get("type") == "main_anhydride":
m_anhydride = item.get("amount")
break
if m_anhydride is None:
raise ValueError("在feeding_order中未找到type为'main_anhydride'的条目")
# 2. 解析 extracted_actuals 获取 actualTargetWeigh 和 actualVolume
if isinstance(extracted_actuals, str):
try:
extracted_actuals_obj = json.loads(extracted_actuals)
except json.JSONDecodeError as e:
raise ValueError(f"extracted_actuals JSON解析失败: {str(e)}")
else:
extracted_actuals_obj = extracted_actuals
# 获取actuals数组
actuals_list = extracted_actuals_obj.get("actuals", [])
if not actuals_list:
# actuals为空,无法自动生成公式,回退到手动模式
print(f"警告: extracted_actuals中actuals数组为空,无法自动生成公式,请手动提供volume_formula")
volume_formula = None # 清空,触发后续的错误检查
else:
# 根据assign_material_name匹配对应的actual数据
# 假设order_code中包含物料名称
matched_actual = None
for actual in actuals_list:
order_code = actual.get("order_code", "")
# 简单匹配:如果order_code包含物料名称
if assign_material_name in order_code:
matched_actual = actual
break
# 如果没有匹配到,使用第一个
if not matched_actual and actuals_list:
matched_actual = actuals_list[0]
if not matched_actual:
raise ValueError("无法从extracted_actuals中获取实际加料量数据")
m_anhydride_titration = matched_actual.get("actualTargetWeigh") # m二酐滴定
v_anhydride_titration = matched_actual.get("actualVolume") # V二酐滴定
if m_anhydride_titration is None or v_anhydride_titration is None:
raise ValueError(f"实际加料量数据不完整: actualTargetWeigh={m_anhydride_titration}, actualVolume={v_anhydride_titration}")
# 3. 构建公式: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
# x_value 格式如 "{{1-2-3}}",保留完整格式(包括花括号)直接替换到公式中
volume_formula = f"1000*({m_anhydride}-{x_value})*{v_anhydride_titration}/{m_anhydride_titration}"
print(f"自动生成滴定公式: {volume_formula}")
print(f" m二酐={m_anhydride}, x={x_value}, V二酐滴定={v_anhydride_titration}, m二酐滴定={m_anhydride_titration}")
elif not volume_formula:
raise ValueError("必须提供 volume_formula 或 (x_value + feeding_order_data + extracted_actuals)")
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
@@ -343,9 +487,288 @@ class BioyondReactionStation(BioyondWorkstation):
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
def _extract_actuals_from_report(self, report) -> Dict[str, Any]:
data = report.get('data') if isinstance(report, dict) else None
actual_target_weigh = None
actual_volume = None
if data:
extra = data.get('extraProperties') or {}
if isinstance(extra, dict):
for v in extra.values():
obj = None
try:
obj = json.loads(v) if isinstance(v, str) else v
except Exception:
obj = None
if isinstance(obj, dict):
tw = obj.get('targetWeigh')
vol = obj.get('volume')
if tw is not None:
try:
actual_target_weigh = float(tw)
except Exception:
pass
if vol is not None:
try:
actual_volume = float(vol)
except Exception:
pass
return {
'actualTargetWeigh': actual_target_weigh,
'actualVolume': actual_volume
}
def extract_actuals_from_batch_reports(self, batch_reports_result: str) -> dict:
print(f"[DEBUG] extract_actuals 收到原始数据: {batch_reports_result[:500]}...") # 打印前500字符
try:
obj = json.loads(batch_reports_result) if isinstance(batch_reports_result, str) else batch_reports_result
if isinstance(obj, dict) and "return_info" in obj:
inner = obj["return_info"]
obj = json.loads(inner) if isinstance(inner, str) else inner
reports = obj.get("reports", []) if isinstance(obj, dict) else []
print(f"[DEBUG] 解析后的 reports 数组长度: {len(reports)}")
except Exception as e:
print(f"[DEBUG] 解析异常: {e}")
reports = []
actuals = []
for i, r in enumerate(reports):
print(f"[DEBUG] 处理 report[{i}]: order_code={r.get('order_code')}, has_extracted={r.get('extracted') is not None}, has_report={r.get('report') is not None}")
order_code = r.get("order_code")
order_id = r.get("order_id")
ex = r.get("extracted")
if isinstance(ex, dict) and (ex.get("actualTargetWeigh") is not None or ex.get("actualVolume") is not None):
print(f"[DEBUG] 从 extracted 字段提取: actualTargetWeigh={ex.get('actualTargetWeigh')}, actualVolume={ex.get('actualVolume')}")
actuals.append({
"order_code": order_code,
"order_id": order_id,
"actualTargetWeigh": ex.get("actualTargetWeigh"),
"actualVolume": ex.get("actualVolume")
})
continue
report = r.get("report")
vals = self._extract_actuals_from_report(report) if report else {"actualTargetWeigh": None, "actualVolume": None}
print(f"[DEBUG] 从 report 字段提取: {vals}")
actuals.append({
"order_code": order_code,
"order_id": order_id,
**vals
})
print(f"[DEBUG] 最终提取的 actuals 数组长度: {len(actuals)}")
result = {
"return_info": json.dumps({"actuals": actuals}, ensure_ascii=False)
}
print(f"[DEBUG] 返回结果: {result}")
return result
def process_temperature_cutoff_report(self, report_request) -> Dict[str, Any]:
try:
data = report_request.data
def _f(v):
try:
return float(v)
except Exception:
return 0.0
self.target_temperature = _f(data.get("targetTemperature"))
self.setting_temperature = _f(data.get("settingTemperature"))
self.in_temperature = _f(data.get("inTemperature"))
self.out_temperature = _f(data.get("outTemperature"))
self.pt100_temperature = _f(data.get("pt100Temperature"))
self.sensor_average_temperature = _f(data.get("sensorAverageTemperature"))
self.speed = _f(data.get("speed"))
self.force = _f(data.get("force"))
self.viscosity = _f(data.get("viscosity"))
self.average_viscosity = _f(data.get("averageViscosity"))
try:
if hasattr(self, "_ros_node") and self._ros_node is not None:
props = [
"in_temperature","out_temperature","pt100_temperature","sensor_average_temperature",
"target_temperature","setting_temperature","viscosity","average_viscosity",
"speed","force"
]
for name in props:
pub = self._ros_node._property_publishers.get(name)
if pub:
pub.publish_property()
frame = data.get("frameCode")
reactor_id = None
try:
reactor_id = self._frame_to_reactor_id.get(int(frame))
except Exception:
reactor_id = None
if reactor_id and hasattr(self._ros_node, "sub_devices"):
child = self._ros_node.sub_devices.get(reactor_id)
if child and hasattr(child, "driver_instance"):
child.driver_instance.update_metrics(data)
pubs = getattr(child.ros_node_instance, "_property_publishers", {})
for name in props:
p = pubs.get(name)
if p:
p.publish_property()
except Exception:
pass
event = {
"frameCode": data.get("frameCode"),
"generateTime": data.get("generateTime"),
"targetTemperature": data.get("targetTemperature"),
"settingTemperature": data.get("settingTemperature"),
"inTemperature": data.get("inTemperature"),
"outTemperature": data.get("outTemperature"),
"pt100Temperature": data.get("pt100Temperature"),
"sensorAverageTemperature": data.get("sensorAverageTemperature"),
"speed": data.get("speed"),
"force": data.get("force"),
"viscosity": data.get("viscosity"),
"averageViscosity": data.get("averageViscosity"),
"request_time": report_request.request_time,
"timestamp": datetime.now().isoformat(),
"reactor_id": self._frame_to_reactor_id.get(int(data.get("frameCode", 0))) if str(data.get("frameCode", "")).isdigit() else None,
}
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data"
base_dir.mkdir(parents=True, exist_ok=True)
out_file = base_dir / "temperature_cutoff_events.json"
try:
existing = json.loads(out_file.read_text(encoding="utf-8")) if out_file.exists() else []
if not isinstance(existing, list):
existing = []
except Exception:
existing = []
existing.append(event)
out_file.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
if hasattr(self, "_ros_node") and self._ros_node is not None:
ns = self._ros_node.namespace
topics = {
"targetTemperature": f"{ns}/metrics/temperature_cutoff/target_temperature",
"settingTemperature": f"{ns}/metrics/temperature_cutoff/setting_temperature",
"inTemperature": f"{ns}/metrics/temperature_cutoff/in_temperature",
"outTemperature": f"{ns}/metrics/temperature_cutoff/out_temperature",
"pt100Temperature": f"{ns}/metrics/temperature_cutoff/pt100_temperature",
"sensorAverageTemperature": f"{ns}/metrics/temperature_cutoff/sensor_average_temperature",
"speed": f"{ns}/metrics/temperature_cutoff/speed",
"force": f"{ns}/metrics/temperature_cutoff/force",
"viscosity": f"{ns}/metrics/temperature_cutoff/viscosity",
"averageViscosity": f"{ns}/metrics/temperature_cutoff/average_viscosity",
}
for k, t in topics.items():
v = data.get(k)
if v is not None:
pub = self._ros_node.create_publisher(Float64, t, 10)
pub.publish(convert_to_ros_msg(Float64, float(v)))
evt_pub = self._ros_node.create_publisher(String, f"{ns}/events/temperature_cutoff", 10)
evt_pub.publish(convert_to_ros_msg(String, json.dumps(event, ensure_ascii=False)))
return {"processed": True, "frame": data.get("frameCode")}
except Exception as e:
return {"processed": False, "error": str(e)}
def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = None, timeout: int = 7200, check_interval: int = 10) -> Dict[str, Any]:
try:
timeout = int(timeout) if timeout else 7200
check_interval = int(check_interval) if check_interval else 10
if not batch_create_result or batch_create_result == "":
raise ValueError("batch_create_result为空")
try:
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
batch_create_result = batch_create_result.replace('[...]', '[]')
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
if isinstance(result_obj, dict) and "return_value" in result_obj:
inner = result_obj.get("return_value")
if isinstance(inner, str):
result_obj = json.loads(inner)
elif isinstance(inner, dict):
result_obj = inner
order_codes = result_obj.get("order_codes", [])
order_ids = result_obj.get("order_ids", [])
except Exception as e:
raise ValueError(f"解析batch_create_result失败: {e}")
if not order_codes or not order_ids:
raise ValueError("缺少order_codes或order_ids")
if not isinstance(order_codes, list):
order_codes = [order_codes]
if not isinstance(order_ids, list):
order_ids = [order_ids]
if len(order_codes) != len(order_ids):
raise ValueError("order_codes与order_ids数量不匹配")
total = len(order_codes)
pending = {c: {"order_id": order_ids[i], "completed": False} for i, c in enumerate(order_codes)}
reports = []
start_time = time.time()
while pending:
elapsed_time = time.time() - start_time
if elapsed_time > timeout:
for oc in list(pending.keys()):
reports.append({
"order_code": oc,
"order_id": pending[oc]["order_id"],
"status": "timeout",
"completion_status": None,
"report": None,
"extracted": None,
"elapsed_time": elapsed_time
})
break
completed_round = []
for oc in list(pending.keys()):
oid = pending[oc]["order_id"]
if oc in self.order_completion_status:
info = self.order_completion_status[oc]
try:
rep = self.hardware_interface.order_report(oid)
if not rep:
rep = {"error": "无法获取报告"}
reports.append({
"order_code": oc,
"order_id": oid,
"status": "completed",
"completion_status": info.get('status'),
"report": rep,
"extracted": self._extract_actuals_from_report(rep),
"elapsed_time": elapsed_time
})
completed_round.append(oc)
del self.order_completion_status[oc]
except Exception as e:
reports.append({
"order_code": oc,
"order_id": oid,
"status": "error",
"completion_status": info.get('status') if 'info' in locals() else None,
"report": None,
"extracted": None,
"error": str(e),
"elapsed_time": elapsed_time
})
completed_round.append(oc)
for oc in completed_round:
del pending[oc]
if pending:
time.sleep(check_interval)
completed_count = sum(1 for r in reports if r['status'] == 'completed')
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
error_count = sum(1 for r in reports if r['status'] == 'error')
final_elapsed_time = time.time() - start_time
summary = {
"total": total,
"completed": completed_count,
"timeout": timeout_count,
"error": error_count,
"elapsed_time": round(final_elapsed_time, 2),
"reports": reports
}
return {
"return_info": json.dumps(summary, ensure_ascii=False)
}
except Exception as e:
raise
def liquid_feeding_beaker(
self,
volume: str = "35000",
volume: str = "350",
assign_material_name: str = "BAPP",
time: str = "0",
torque_variation: int = 1,
@@ -355,7 +778,7 @@ class BioyondReactionStation(BioyondWorkstation):
"""液体进料烧杯
Args:
volume: 分液量(μL)
volume: 分液量(g)
assign_material_name: 物料名称(试剂瓶位)
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
@@ -489,6 +912,106 @@ class BioyondReactionStation(BioyondWorkstation):
"""
return self.hardware_interface.create_order(json_str)
def hard_delete_merged_workflows(self, workflow_ids: List[str]) -> Dict[str, Any]:
"""
调用新接口:硬删除合并后的工作流
Args:
workflow_ids: 要删除的工作流ID数组
Returns:
删除结果
"""
try:
if not isinstance(workflow_ids, list):
raise ValueError("workflow_ids必须是字符串数组")
return self._delete_project_api("/api/lims/order/workflows", workflow_ids)
except Exception as e:
print(f"❌ 硬删除异常: {str(e)}")
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
# ==================== 项目接口通用方法 ====================
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
"""项目接口通用POST调用
参数:
endpoint: 接口路径(例如 /api/lims/order/skip-titration-steps
data: 请求体中的 data 字段内容
返回:
dict: 服务端响应,失败时返回 {code:0,message,...}
"""
request_data = {
"apiKey": API_CONFIG["api_key"],
"requestTime": self.hardware_interface.get_current_time_iso8601(),
"data": data
}
print(f"\n📤 项目POST请求: {self.hardware_interface.host}{endpoint}")
print(json.dumps(request_data, indent=4, ensure_ascii=False))
try:
response = requests.post(
f"{self.hardware_interface.host}{endpoint}",
json=request_data,
headers={"Content-Type": "application/json"},
timeout=30
)
result = response.json()
if result.get("code") == 1:
print("✅ 请求成功")
else:
print(f"❌ 请求失败: {result.get('message','未知错误')}")
return result
except json.JSONDecodeError:
print("❌ 非JSON响应")
return {"code": 0, "message": "非JSON响应", "timestamp": int(time.time())}
except requests.exceptions.Timeout:
print("❌ 请求超时")
return {"code": 0, "message": "请求超时", "timestamp": int(time.time())}
except requests.exceptions.RequestException as e:
print(f"❌ 网络异常: {str(e)}")
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
def _delete_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
"""项目接口通用DELETE调用
参数:
endpoint: 接口路径(例如 /api/lims/order/workflows
data: 请求体中的 data 字段内容
返回:
dict: 服务端响应,失败时返回 {code:0,message,...}
"""
request_data = {
"apiKey": API_CONFIG["api_key"],
"requestTime": self.hardware_interface.get_current_time_iso8601(),
"data": data
}
print(f"\n📤 项目DELETE请求: {self.hardware_interface.host}{endpoint}")
print(json.dumps(request_data, indent=4, ensure_ascii=False))
try:
response = requests.delete(
f"{self.hardware_interface.host}{endpoint}",
json=request_data,
headers={"Content-Type": "application/json"},
timeout=30
)
result = response.json()
if result.get("code") == 1:
print("✅ 请求成功")
else:
print(f"❌ 请求失败: {result.get('message','未知错误')}")
return result
except json.JSONDecodeError:
print("❌ 非JSON响应")
return {"code": 0, "message": "非JSON响应", "timestamp": int(time.time())}
except requests.exceptions.Timeout:
print("❌ 请求超时")
return {"code": 0, "message": "请求超时", "timestamp": int(time.time())}
except requests.exceptions.RequestException as e:
print(f"❌ 网络异常: {str(e)}")
return {"code": 0, "message": str(e), "timestamp": int(time.time())}
# ==================== 工作流执行核心方法 ====================
def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]:
@@ -519,69 +1042,6 @@ class BioyondReactionStation(BioyondWorkstation):
print(f"错误:处理工作流失败: {e}")
return []
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
"""
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
Args:
workflow_name: 合并后的工作流名称
task_name: 任务名称
Returns:
任务创建结果
"""
web_workflow_list = self.get_workflow_sequence()
print(f"\n{'='*60}")
print(f"📋 处理网页工作流列表: {web_workflow_list}")
print(f"{'='*60}")
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
workflows_result = self.process_web_workflows(web_workflow_json)
if not workflows_result:
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
print(f"workflows_result 类型: {type(workflows_result)}")
print(f"workflows_result 内容: {workflows_result}")
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
merge_data = {
"name": workflow_name,
"workflows": workflows_with_params
}
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
if not merged_workflow:
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
order_params = [{
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
"orderName": task_name,
"workFlowId": workflow_id,
"borderNumber": 1,
"paramValues": {}
}]
result = self.create_order(json.dumps(order_params))
if not result:
return self._create_error_result("创建任务失败", "create_order")
# 清空工作流序列和参数,防止下次执行时累积重复
self.pending_task_params = []
self.clear_workflows() # 清空工作流序列,避免重复累积
# print(f"\n✅ 任务创建成功: {result}")
# print(f"\n✅ 任务创建成功")
print(f"{'='*60}\n")
return json.dumps({"success": True, "result": result})
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
"""
构建带参数的工作流列表
@@ -781,3 +1241,90 @@ class BioyondReactionStation(BioyondWorkstation):
print(f" ❌ 工作流ID验证失败: {e}")
print(f" 💡 将重新合并工作流")
return False
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
"""
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
Args:
workflow_name: 合并后的工作流名称
task_name: 任务名称
Returns:
任务创建结果
"""
web_workflow_list = self.get_workflow_sequence()
print(f"\n{'='*60}")
print(f"📋 处理网页工作流列表: {web_workflow_list}")
print(f"{'='*60}")
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
workflows_result = self.process_web_workflows(web_workflow_json)
if not workflows_result:
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
print(f"workflows_result 类型: {type(workflows_result)}")
print(f"workflows_result 内容: {workflows_result}")
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
merge_data = {
"name": workflow_name,
"workflows": workflows_with_params
}
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
if not merged_workflow:
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
order_params = [{
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
"orderName": task_name,
"workFlowId": workflow_id,
"borderNumber": 1,
"paramValues": {}
}]
result = self.create_order(json.dumps(order_params))
if not result:
return self._create_error_result("创建任务失败", "create_order")
# 清空工作流序列和参数,防止下次执行时累积重复
self.pending_task_params = []
self.clear_workflows() # 清空工作流序列,避免重复累积
# print(f"\n✅ 任务创建成功: {result}")
# print(f"\n✅ 任务创建成功")
print(f"{'='*60}\n")
# 返回结果,包含合并后的工作流数据和订单参数
return json.dumps({
"success": True,
"result": result,
"merged_workflow": merged_workflow,
"order_params": order_params
})
# ==================== 反应器操作接口 ====================
def skip_titration_steps(self, preintake_id: str) -> Dict[str, Any]:
"""跳过当前正在进行的滴定步骤
Args:
preintake_id: 通量ID
Returns:
Dict[str, Any]: 服务器响应,包含状态码、消息和时间戳
"""
try:
return self._post_project_api("/api/lims/order/skip-titration-steps", preintake_id)
except Exception as e:
print(f"❌ 跳过滴定异常: {str(e)}")
return {"code": 0, "message": str(e), "timestamp": int(time.time())}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -147,7 +147,7 @@ class WorkstationBase(ABC):
def __init__(
self,
deck: Deck,
deck: Optional[Deck],
*args,
**kwargs, # 必须有kwargs
):
@@ -349,5 +349,5 @@ class WorkstationBase(ABC):
class ProtocolNode(WorkstationBase):
def __init__(self, deck: Optional[PLRResource], *args, **kwargs):
def __init__(self, protocol_type: List[str], deck: Optional[PLRResource], *args, **kwargs):
super().__init__(deck, *args, **kwargs)

View File

@@ -22,6 +22,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
from dataclasses import dataclass, asdict
from datetime import datetime
from pathlib import Path
from unilabos.utils.log import logger
@@ -76,6 +77,14 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
try:
payload_for_log = {"method": "POST", **request_data}
self._save_raw_request(endpoint, payload_for_log)
if hasattr(self.workstation, '_reports_received_count'):
self.workstation._reports_received_count += 1
except Exception:
pass
# 统一的报送端点路由基于LIMS协议规范
if endpoint == '/report/step_finish':
response = self._handle_step_finish_report(request_data)
@@ -90,6 +99,8 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
response = self._handle_material_change_report(request_data)
elif endpoint == '/report/error_handling':
response = self._handle_error_handling_report(request_data)
elif endpoint == '/report/temperature-cutoff':
response = self._handle_temperature_cutoff_report(request_data)
# 保留LIMS协议端点以兼容现有系统
elif endpoint == '/LIMS/step_finish':
response = self._handle_step_finish_report(request_data)
@@ -107,7 +118,8 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
"/report/order_finish",
"/report/batch_update",
"/report/material_change",
"/report/error_handling"
"/report/error_handling",
"/report/temperature-cutoff"
]}
)
@@ -128,6 +140,11 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
parsed_path = urlparse(self.path)
endpoint = parsed_path.path
try:
self._save_raw_request(endpoint, {"method": "GET"})
except Exception:
pass
if endpoint == '/status':
response = self._handle_status_check()
elif endpoint == '/health':
@@ -318,6 +335,50 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
message=f"任务完成报送处理失败: {str(e)}"
)
def _handle_temperature_cutoff_report(self, request_data: Dict[str, Any]) -> HttpResponse:
try:
required_fields = ['token', 'request_time', 'data']
if missing := [f for f in required_fields if f not in request_data]:
return HttpResponse(success=False, message=f"缺少必要字段: {', '.join(missing)}")
data = request_data['data']
metrics = [
'frameCode',
'generateTime',
'targetTemperature',
'settingTemperature',
'inTemperature',
'outTemperature',
'pt100Temperature',
'sensorAverageTemperature',
'speed',
'force',
'viscosity',
'averageViscosity'
]
if miss := [f for f in metrics if f not in data]:
return HttpResponse(success=False, message=f"data字段缺少必要内容: {', '.join(miss)}")
report_request = WorkstationReportRequest(
token=request_data['token'],
request_time=request_data['request_time'],
data=data
)
result = {}
if hasattr(self.workstation, 'process_temperature_cutoff_report'):
result = self.workstation.process_temperature_cutoff_report(report_request)
return HttpResponse(
success=True,
message=f"温度/粘度报送已处理: 帧{data['frameCode']}",
acknowledgment_id=f"TEMP_CUTOFF_{int(time.time()*1000)}_{data['frameCode']}",
data=result
)
except Exception as e:
logger.error(f"处理温度/粘度报送失败: {e}\n{traceback.format_exc()}")
return HttpResponse(success=False, message=f"温度/粘度报送处理失败: {str(e)}")
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
"""处理批量报送"""
try:
@@ -488,11 +549,18 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
def _handle_status_check(self) -> HttpResponse:
"""处理状态查询"""
try:
# 安全地获取 device_id
device_id = "unknown"
if hasattr(self.workstation, 'device_id'):
device_id = self.workstation.device_id
elif hasattr(self.workstation, '_ros_node') and hasattr(self.workstation._ros_node, 'device_id'):
device_id = self.workstation._ros_node.device_id
return HttpResponse(
success=True,
message="工作站报送服务正常运行",
data={
"workstation_id": self.workstation.device_id,
"workstation_id": device_id,
"service_type": "unified_reporting_service",
"uptime": time.time() - getattr(self.workstation, '_start_time', time.time()),
"reports_received": getattr(self.workstation, '_reports_received_count', 0),
@@ -503,6 +571,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
"POST /report/batch_update",
"POST /report/material_change",
"POST /report/error_handling",
"POST /report/temperature-cutoff",
"GET /status",
"GET /health"
]
@@ -540,6 +609,22 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
"""重写日志方法"""
logger.debug(f"HTTP请求: {format % args}")
def _save_raw_request(self, endpoint: str, request_data: Dict[str, Any]) -> None:
try:
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
base_dir.mkdir(parents=True, exist_ok=True)
log_path = getattr(self.workstation, "_http_log_path", None)
log_file = Path(log_path) if log_path else (base_dir / f"http_{int(time.time()*1000)}.log")
payload = {
"endpoint": endpoint,
"received_at": datetime.now().isoformat(),
"body": request_data
}
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
except Exception:
pass
class WorkstationHTTPService:
"""工作站HTTP服务"""
@@ -565,12 +650,23 @@ class WorkstationHTTPService:
# 创建HTTP服务器
self.server = HTTPServer((self.host, self.port), handler_factory)
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
base_dir.mkdir(parents=True, exist_ok=True)
session_log = base_dir / f"http_{int(time.time()*1000)}.log"
setattr(self.workstation, "_http_log_path", str(session_log))
# 安全地获取 device_id 用于线程命名
device_id = "unknown"
if hasattr(self.workstation, 'device_id'):
device_id = self.workstation.device_id
elif hasattr(self.workstation, '_ros_node') and hasattr(self.workstation._ros_node, 'device_id'):
device_id = self.workstation._ros_node.device_id
# 在单独线程中运行服务器
self.server_thread = threading.Thread(
target=self._run_server,
daemon=True,
name=f"WorkstationHTTP-{self.workstation.device_id}"
name=f"WorkstationHTTP-{device_id}"
)
self.running = True
@@ -585,6 +681,7 @@ class WorkstationHTTPService:
logger.info("扩展报送端点:")
logger.info(" - POST /report/material_change # 物料变更报送")
logger.info(" - POST /report/error_handling # 错误处理报送")
logger.info(" - POST /report/temperature-cutoff # 温度/粘度报送")
logger.info("兼容端点:")
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
@@ -668,7 +765,7 @@ __all__ = [
if __name__ == "__main__":
# 简单测试HTTP服务
class DummyWorkstation:
class BioyondWorkstation:
device_id = "WS-001"
def process_step_finish_report(self, report_request):
@@ -686,7 +783,10 @@ if __name__ == "__main__":
def handle_external_error(self, error_data):
return {"handled": True}
workstation = DummyWorkstation()
def process_temperature_cutoff_report(self, report_request):
return {"processed": True, "metrics": report_request.data}
workstation = BioyondWorkstation()
http_service = WorkstationHTTPService(workstation)
try:
@@ -709,4 +809,3 @@ if __name__ == "__main__":
except Exception as e:
print(f"服务器运行错误: {e}")
http_service.stop()

File diff suppressed because it is too large Load Diff

View File

@@ -174,6 +174,35 @@ bioyond_dispensing_station:
title: query_resource_by_name参数
type: object
type: UniLabJsonCommand
auto-transfer_materials_to_reaction_station:
feedback: {}
goal: {}
goal_default:
target_device_id: null
transfer_groups: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
target_device_id:
type: string
transfer_groups:
type: array
required:
- target_device_id
- transfer_groups
type: object
result: {}
required:
- goal
title: transfer_materials_to_reaction_station参数
type: object
type: UniLabJsonCommand
auto-workflow_sample_locations:
feedback: {}
goal: {}
@@ -591,56 +620,6 @@ bioyond_dispensing_station:
title: DispenStationSolnPrep
type: object
type: DispenStationSolnPrep
transfer_materials_to_reaction_station:
feedback: {}
goal:
target_device_id: target_device_id
transfer_groups: transfer_groups
goal_default:
target_device_id: ''
transfer_groups: ''
handles: {}
placeholder_keys:
target_device_id: unilabos_devices
result: {}
schema:
description: 将配液站完成的物料(溶液、样品等)转移到指定反应站的堆栈库位。支持配置多组转移任务,每组包含物料名称、目标堆栈和目标库位。
properties:
feedback: {}
goal:
properties:
target_device_id:
description: 目标反应站设备ID从设备列表中选择所有转移组都使用同一个目标设备
type: string
transfer_groups:
description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组
items:
properties:
materials:
description: 物料名称手动输入系统将通过RPC查询验证
type: string
target_sites:
description: 目标库位(手动输入,如"A01"
type: string
target_stack:
description: 目标堆栈名称(手动输入,如"堆栈1左"
type: string
required:
- materials
- target_stack
- target_sites
type: object
type: array
required:
- target_device_id
- transfer_groups
type: object
result: {}
required:
- goal
title: transfer_materials_to_reaction_station参数
type: object
type: UniLabJsonCommand
wait_for_multiple_orders_and_get_reports:
feedback: {}
goal:

View File

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

View File

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

View File

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

View File

@@ -9333,34 +9333,7 @@ liquid_handler.prcxi:
touch_tip: false
use_channels:
- 0
handles:
input:
- data_key: liquid
data_source: handle
data_type: resource
handler_key: sources
label: sources
- data_key: liquid
data_source: executor
data_type: resource
handler_key: targets
label: targets
- data_key: liquid
data_source: executor
data_type: resource
handler_key: tip_rack
label: tip_rack
output:
- data_key: liquid
data_source: handle
data_type: resource
handler_key: sources_out
label: sources
- data_key: liquid
data_source: executor
data_type: resource
handler_key: targets_out
label: targets
handles: {}
placeholder_keys:
sources: unilabos_resources
targets: unilabos_resources

View File

@@ -1,8 +1,6 @@
neware_battery_test_system:
category:
- neware_battery_test_system
- neware
- battery_test
class:
action_value_mappings:
auto-post_init:
@@ -72,38 +70,6 @@ neware_battery_test_system:
title: test_connection参数
type: object
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:
feedback: {}
goal:
@@ -253,9 +219,7 @@ neware_battery_test_system:
goal_default:
string: ''
handles: {}
result:
return_info: return_info
success: success
result: {}
schema:
description: ''
properties:
@@ -288,56 +252,6 @@ neware_battery_test_system:
title: StrSingleInput
type: object
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:
feedback: {}
goal: {}
@@ -370,7 +284,7 @@ neware_battery_test_system:
- goal
type: object
type: UniLabJsonCommand
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
module: unilabos.devices.battery.neware_battery_test_system:NewareBatteryTestSystem
status_types:
channel_status: dict
connection_info: dict
@@ -380,7 +294,7 @@ neware_battery_test_system:
total_channels: int
type: python
config_info: []
description: 新威电池测试系统驱动,提供720个通道的电池测试状态监控、物料管理和CSV批量提交功能。支持TCP通信实现远程控制包含完整的物料管理系统2盘电池状态映射以及从CSV文件批量提交测试任务的能力
description: 新威电池测试系统驱动,支持720个通道的电池测试状态监控和数据导出。通过TCP通信实现远程控制包含完整的物料管理系统,支持2盘电池状态映射和监控
handles: []
icon: ''
init_param_schema:
@@ -396,13 +310,13 @@ neware_battery_test_system:
port:
type: integer
size_x:
default: 50
default: 500.0
type: number
size_y:
default: 50
default: 500.0
type: number
size_z:
default: 20
default: 2000.0
type: number
timeout:
type: integer

View File

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

View File

@@ -222,7 +222,7 @@ class Registry:
abs_path = Path(path).absolute()
resource_path = abs_path / "resources"
files = list(resource_path.glob("*/*.yaml"))
logger.trace(f"[UniLab Registry] load resources? {resource_path.exists()}, total: {len(files)}")
logger.debug(f"[UniLab Registry] resources: {resource_path.exists()}, total: {len(files)}")
current_resource_number = len(self.resource_type_registry) + 1
for i, file in enumerate(files):
with open(file, encoding="utf-8", mode="r") as f:

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
"""Battery-related resource classes for coin cell assembly"""

View File

@@ -1,45 +0,0 @@
"""
瓶架类定义 - 用于纽扣电池组装工作站
Bottle Carrier Resource Classes
"""
from __future__ import annotations
from pylabrobot.resources import ResourceHolder
from pylabrobot.resources.utils import create_ordered_items_2d
from unilabos.resources.itemized_carrier import ItemizedCarrier
def YIHUA_Electrolyte_12VialCarrier(name: str) -> ItemizedCarrier:
"""依华电解液12瓶架 - 3x4布局
Args:
name: 瓶架名称
Returns:
ItemizedCarrier: 包含12个瓶位的瓶架
"""
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4,
num_items_y=3,
dx=10.0,
dy=10.0,
dz=5.0,
item_dx=70.0,
item_dy=26.67,
size_x=60.0,
size_y=20.0,
size_z=70.0,
)
return ItemizedCarrier(
name=name,
size_x=300.0,
size_y=100.0,
size_z=80.0,
num_items_x=4,
num_items_y=3,
sites=sites,
category="bottle_carrier",
)

View File

@@ -1,67 +0,0 @@
"""
电极片类定义
Electrode Sheet Resource Classes
"""
from __future__ import annotations
from typing import Any, Dict, Optional
from pylabrobot.resources.resource import Resource
class ElectrodeSheet(Resource):
"""电极片类 - 用于纽扣电池组装"""
def __init__(
self,
name: str,
size_x: float = 12.0,
size_y: float = 12.0,
size_z: float = 0.1,
category: str = "electrode_sheet",
electrode_type: str = "anode", # "anode" 负极, "cathode" 正极, "separator" 隔膜
**kwargs
):
"""初始化电极片
Args:
name: 电极片名称
size_x: X方向尺寸 (mm)
size_y: Y方向尺寸 (mm)
size_z: Z方向尺寸/厚度 (mm)
category: 类别
electrode_type: 电极类型
"""
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
**kwargs
)
self._electrode_type = electrode_type
self._unilabos_state: Dict[str, Any] = {
"electrode_type": electrode_type,
"material": "",
"thickness": size_z,
}
@property
def electrode_type(self) -> str:
"""获取电极类型"""
return self._electrode_type
def load_state(self, state: Dict[str, Any]) -> None:
"""加载状态"""
super().load_state(state)
if isinstance(state, dict):
self._unilabos_state.update(state)
def serialize_state(self) -> Dict[str, Any]:
"""序列化状态"""
data = super().serialize_state()
data.update(self._unilabos_state)
return data

View File

@@ -1,152 +0,0 @@
"""
弹夹架类定义 - 用于纽扣电池组装工作站
Magazine Holder Resource Classes
"""
from __future__ import annotations
from typing import List, Optional
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources import ResourceHolder
from pylabrobot.resources.utils import create_ordered_items_2d
from unilabos.resources.itemized_carrier import ItemizedCarrier
def MagazineHolder_4_Cathode(name: str) -> ItemizedCarrier:
"""正极&铝箔弹夹 - 4个洞位 (2x2布局)
Args:
name: 弹夹名称
Returns:
ItemizedCarrier: 包含4个槽位的弹夹架
"""
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=2,
num_items_y=2,
dx=10.0,
dy=10.0,
dz=0.0,
item_dx=50.0,
item_dy=30.0,
size_x=40.0,
size_y=25.0,
size_z=40.0,
)
return ItemizedCarrier(
name=name,
size_x=120.0,
size_y=80.0,
size_z=50.0,
num_items_x=2,
num_items_y=2,
sites=sites,
category="magazine_holder",
)
def MagazineHolder_6_Cathode(name: str) -> ItemizedCarrier:
"""正极壳&平垫片弹夹 - 6个洞位 (2x3布局)
Args:
name: 弹夹名称
Returns:
ItemizedCarrier: 包含6个槽位的弹夹架
"""
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=10.0,
dy=10.0,
dz=0.0,
item_dx=40.0,
item_dy=30.0,
size_x=35.0,
size_y=25.0,
size_z=40.0,
)
return ItemizedCarrier(
name=name,
size_x=150.0,
size_y=80.0,
size_z=50.0,
num_items_x=3,
num_items_y=2,
sites=sites,
category="magazine_holder",
)
def MagazineHolder_6_Anode(name: str) -> ItemizedCarrier:
"""负极壳&弹垫片弹夹 - 6个洞位 (2x3布局)
Args:
name: 弹夹名称
Returns:
ItemizedCarrier: 包含6个槽位的弹夹架
"""
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=10.0,
dy=10.0,
dz=0.0,
item_dx=40.0,
item_dy=30.0,
size_x=35.0,
size_y=25.0,
size_z=40.0,
)
return ItemizedCarrier(
name=name,
size_x=150.0,
size_y=80.0,
size_z=50.0,
num_items_x=3,
num_items_y=2,
sites=sites,
category="magazine_holder",
)
def MagazineHolder_6_Battery(name: str) -> ItemizedCarrier:
"""成品弹夹 - 6个洞位 (3x2布局)
Args:
name: 弹夹名称
Returns:
ItemizedCarrier: 包含6个槽位的弹夹架
"""
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=10.0,
dy=10.0,
dz=0.0,
item_dx=33.0,
item_dy=40.0,
size_x=30.0,
size_y=35.0,
size_z=40.0,
)
return ItemizedCarrier(
name=name,
size_x=120.0,
size_y=100.0,
size_z=50.0,
num_items_x=3,
num_items_y=2,
sites=sites,
category="magazine_holder",
)

View File

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

View File

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

View File

@@ -1,209 +0,0 @@
from unilabos.resources.warehouse import WareHouse, YB_warehouse_factory
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
num_items_z=4,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
"""创建BioYond 4x1x2仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
num_items_z=2,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
removed_positions=None
)
# 定义benyond的堆栈
def bioyond_warehouse_1x2x2(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=2,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="YB_warehouse",
)
def bioyond_warehouse_2x2x1(name: str) -> WareHouse:
"""创建BioYond 2x2x1仓库自动堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=2,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="YB_warehouse",
)
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=10,
num_items_y=1,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_y=3,
num_items_z=3,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_2x1x3(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=1,
num_items_z=3,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_3x3x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=3,
num_items_y=3,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=5,
num_items_y=1,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=3,
num_items_y=3,
num_items_z=1,
dx=12.0,
dy=12.0,
dz=12.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
"""创建BioYond开关盖加液模块台面"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=5,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
removed_positions=None
)
def bioyond_warehouse_3x5x1(name: str) -> WareHouse:
"""创建BioYond 3x5x1仓库手动堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=3,
num_items_y=5,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_20x1x1(name: str) -> WareHouse:
"""创建BioYond 20x1x1仓库粉末加样头堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=20,
num_items_y=1,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)

View File

@@ -0,0 +1,311 @@
from unilabos.resources.warehouse import WareHouse, warehouse_factory
# ================ 反应站相关堆栈 ================
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 4x4x1仓库 (左侧堆栈: A01D04)
使用行优先排序,前端展示为:
A01 | A02 | A03 | A04
B01 | B02 | B03 | B04
C01 | C02 | C03 | C04
D01 | D02 | D03 | D04
"""
return warehouse_factory(
name=name,
num_items_x=4, # 4列
num_items_y=4, # 4行
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=147.0,
item_dy=106.0,
item_dz=130.0,
category="warehouse",
col_offset=0, # 从01开始: A01, A02, A03, A04
layout="row-major", # ⭐ 改为行优先排序
)
def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
"""创建BioYond 4x4x1仓库 (右侧堆栈: A05D08)"""
return warehouse_factory(
name=name,
num_items_x=4,
num_items_y=4,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=147.0,
item_dy=106.0,
item_dz=130.0,
category="warehouse",
col_offset=4, # 从05开始: A05, A06, A07, A08
layout="row-major", # ⭐ 改为行优先排序
)
def bioyond_warehouse_density_vial(name: str) -> WareHouse:
"""创建测量小瓶仓库(测密度) A01B03"""
return warehouse_factory(
name=name,
num_items_x=3, # 3列01-03
num_items_y=2, # 2行A-B
num_items_z=1, # 1层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=40.0,
item_dy=40.0,
item_dz=50.0,
# 用更小的 resource_size 来表现 "小点的孔位"
resource_size_x=30.0,
resource_size_y=30.0,
resource_size_z=12.0,
category="warehouse",
col_offset=0,
layout="row-major",
)
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
"""创建BioYond站内试剂存放堆栈A01A02, 1行×2列"""
return warehouse_factory(
name=name,
num_items_x=2, # 2列01-02
num_items_y=1, # 1行A
num_items_z=1, # 1层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
"""创建BioYond站内Tip盒堆栈A01B03用于存放枪头盒"""
return warehouse_factory(
name=name,
num_items_x=3, # 3列01-03
num_items_y=2, # 2行A-B
num_items_z=1, # 1层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
col_offset=0,
layout="row-major",
)
def bioyond_warehouse_liquid_preparation(name: str) -> WareHouse:
"""已弃用,创建BioYond移液站内10%分装液体准备仓库A01B04"""
return warehouse_factory(
name=name,
num_items_x=4, # 4列01-04
num_items_y=2, # 2行A-B
num_items_z=1, # 1层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
col_offset=0,
layout="row-major",
)
# ================ 配液站相关堆栈 ================
def bioyond_warehouse_reagent_stack(name: str) -> WareHouse:
"""创建BioYond 试剂堆栈 2x4x1 (2行×4列: A01-A04, B01-B04)
使用行优先排序,前端展示为:
A01 | A02 | A03 | A04
B01 | B02 | B03 | B04
"""
return warehouse_factory(
name=name,
num_items_x=4, # 4列 (01-04)
num_items_y=2, # 2行 (A-B)
num_items_z=1, # 1层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=147.0,
item_dy=106.0,
item_dz=130.0,
category="warehouse",
col_offset=0, # 从01开始
layout="row-major", # ⭐ 使用行优先排序: A01,A02,A03,A04, B01,B02,B03,B04
)
# 定义bioyond的堆栈
# =================== Other ===================
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
"""创建BioYond 4x2x1仓库"""
return warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
num_items_z=2,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
removed_positions=None
)
def bioyond_warehouse_1x2x2(name: str) -> WareHouse:
"""创建BioYond 1x2x2仓库"""
return warehouse_factory(
name=name,
num_items_x=1,
num_items_y=2,
num_items_z=2,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
"""创建BioYond 10x1x1仓库"""
return warehouse_factory(
name=name,
num_items_x=10,
num_items_y=1,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
"""创建BioYond 1x3x3仓库"""
return warehouse_factory(
name=name,
num_items_x=1,
num_items_y=3,
num_items_z=3,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_2x1x3(name: str) -> WareHouse:
"""创建BioYond 2x1x3仓库"""
return warehouse_factory(
name=name,
num_items_x=2,
num_items_y=1,
num_items_z=3,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_3x3x1(name: str) -> WareHouse:
"""创建BioYond 3x3x1仓库"""
return warehouse_factory(
name=name,
num_items_x=3,
num_items_y=3,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
"""已弃用创建BioYond 5x1x1仓库"""
return warehouse_factory(
name=name,
num_items_x=5,
num_items_y=1,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
"""已弃用创建BioYond 3x3x1仓库"""
return warehouse_factory(
name=name,
num_items_x=3,
num_items_y=3,
num_items_z=1,
dx=12.0,
dy=12.0,
dz=12.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
"""创建BioYond开关盖加液模块台面"""
return warehouse_factory(
name=name,
num_items_x=2,
num_items_y=5,
num_items_z=1,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
removed_positions=None
)
def bioyond_warehouse_1x8x4(name: str) -> WareHouse:
"""创建BioYond 8x4x1反应站堆栈A01D08"""
return warehouse_factory(
name=name,
num_items_x=8, # 8列01-08
num_items_y=4, # 4行A-D
num_items_z=1, # 1层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=147.0,
item_dy=106.0,
item_dz=130.0,
category="warehouse",
)

View File

@@ -42,7 +42,7 @@ def canonicalize_nodes_data(
Returns:
ResourceTreeSet: 标准化后的资源树集合
"""
print_status(f"{len(nodes)} Resources loaded", "info")
print_status(f"{len(nodes)} Resources loaded:", "info")
# 第一步基本预处理处理graphml的label字段
outer_host_node_id = None
@@ -97,7 +97,7 @@ def canonicalize_nodes_data(
for node in nodes:
try:
# print_status(f"DeviceId: {node['id']}, Class: {node['class']}", "info")
print_status(f"DeviceId: {node['id']}, Class: {node['class']}", "info")
# 使用标准化方法
resource_instance = ResourceDictInstance.get_resource_instance_from_dict(node)
known_nodes[node["id"]] = resource_instance

View File

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

View File

@@ -582,7 +582,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
except Exception as e:
self.lab_logger().error(f"更新资源uuid失败: {e}")
self.lab_logger().error(traceback.format_exc())
self.lab_logger().trace(f"资源更新结果: {response}")
self.lab_logger().debug(f"资源更新结果: {response}")
async def get_resource(self, resources_uuid: List[str], with_children: bool = True) -> ResourceTreeSet:
"""
@@ -1164,6 +1164,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
execution_error = traceback.format_exc()
break
##### self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}")
time_start = time.time()
time_overall = 100
future = None
@@ -1171,36 +1172,35 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 将阻塞操作放入线程池执行
if asyncio.iscoroutinefunction(ACTION):
try:
self.lab_logger().trace(f"异步执行动作 {ACTION}")
def _handle_future_exception(fut: Future):
##### self.lab_logger().info(f"异步执行动作 {ACTION}")
future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs)
def _handle_future_exception(fut):
nonlocal execution_error, execution_success, action_return_value
try:
action_return_value = fut.result()
if isinstance(action_return_value, BaseException):
raise action_return_value
execution_success = True
except Exception as _:
except Exception as e:
execution_error = traceback.format_exc()
error(
f"异步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}"
)
future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs)
future.add_done_callback(_handle_future_exception)
except Exception as e:
execution_error = traceback.format_exc()
execution_success = False
self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}")
else:
self.lab_logger().trace(f"同步执行动作 {ACTION}")
##### self.lab_logger().info(f"同步执行动作 {ACTION}")
future = self._executor.submit(ACTION, **action_kwargs)
def _handle_future_exception(fut: Future):
def _handle_future_exception(fut):
nonlocal execution_error, execution_success, action_return_value
try:
action_return_value = fut.result()
execution_success = True
except Exception as _:
except Exception as e:
execution_error = traceback.format_exc()
error(
f"同步任务 {ACTION.__name__} 报错了\n{traceback.format_exc()}\n原始输入:{action_kwargs}"
@@ -1305,7 +1305,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
get_result_info_str(execution_error, execution_success, action_return_value),
)
self.lab_logger().trace(f"动作 {action_name} 完成并返回结果")
##### self.lab_logger().info(f"动作 {action_name} 完成并返回结果")
return result_msg
return execute_callback
@@ -1540,29 +1540,17 @@ class ROS2DeviceNode:
这个类封装了设备类实例和ROS2节点的功能提供ROS2接口。
它不继承设备类,而是通过代理模式访问设备类的属性和方法。
"""
@staticmethod
async def safe_task_wrapper(trace_callback, func, **kwargs):
try:
if callable(trace_callback):
trace_callback(await func(**kwargs))
return await func(**kwargs)
except Exception as e:
if callable(trace_callback):
trace_callback(e)
return e
@classmethod
def run_async_func(cls, func, trace_error=True, inner_trace_callback=None, **kwargs) -> Task:
def _handle_future_exception(fut: Future):
def run_async_func(cls, func, trace_error=True, **kwargs) -> Task:
def _handle_future_exception(fut):
try:
ret = fut.result()
if isinstance(ret, BaseException):
raise ret
fut.result()
except Exception as e:
error(f"异步任务 {func.__name__} 获取结果失败")
error(f"异步任务 {func.__name__} 报错了")
error(traceback.format_exc())
future = rclpy.get_global_executor().create_task(ROS2DeviceNode.safe_task_wrapper(inner_trace_callback, func, **kwargs))
future = rclpy.get_global_executor().create_task(func(**kwargs))
if trace_error:
future.add_done_callback(_handle_future_exception)
return future

View File

@@ -664,7 +664,7 @@ class HostNode(BaseROS2DeviceNode):
if bCreate:
self.lab_logger().trace(f"Status created: {device_id}.{property_name} = {msg.data}")
else:
self.lab_logger().trace(f"Status updated: {device_id}.{property_name} = {msg.data}")
self.lab_logger().debug(f"Status updated: {device_id}.{property_name} = {msg.data}")
def send_goal(
self,
@@ -718,7 +718,7 @@ class HostNode(BaseROS2DeviceNode):
feedback_callback=lambda feedback_msg: self.feedback_callback(item, action_id, feedback_msg),
goal_uuid=goal_uuid_obj,
)
future.add_done_callback(lambda f: self.goal_response_callback(item, action_id, f))
future.add_done_callback(lambda future: self.goal_response_callback(item, action_id, future))
def goal_response_callback(self, item: "QueueItem", action_id: str, future) -> None:
"""目标响应回调"""
@@ -729,11 +729,9 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().info(f"[Host Node] Goal {action_id} ({item.job_id}) accepted")
self._goals[item.job_id] = goal_handle
goal_future = goal_handle.get_result_async()
goal_future.add_done_callback(
lambda f: self.get_result_callback(item, action_id, f)
goal_handle.get_result_async().add_done_callback(
lambda future: self.get_result_callback(item, action_id, future)
)
goal_future.result()
def feedback_callback(self, item: "QueueItem", action_id: str, feedback_msg) -> None:
"""反馈回调"""
@@ -1157,12 +1155,11 @@ class HostNode(BaseROS2DeviceNode):
响应对象,包含查询到的资源
"""
try:
from unilabos.app.web import http_client
data = json.loads(request.command)
if "uuid" in data and data["uuid"] is not None:
http_req = http_client.resource_tree_get([data["uuid"]], data["with_children"])
http_req = self.bridges[-1].resource_tree_get([data["uuid"]], data["with_children"])
elif "id" in data and data["id"].startswith("/"):
http_req = http_client.resource_get(data["id"], data["with_children"])
http_req = self.bridges[-1].resource_get(data["id"], data["with_children"])
else:
raise ValueError("没有使用正确的物料 id 或 uuid")
response.response = json.dumps(http_req["data"])

View File

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

View File

@@ -66,8 +66,8 @@ class ResourceDict(BaseModel):
klass: str = Field(alias="class", description="Resource class name")
pose: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition)
config: Dict[str, Any] = Field(description="Resource configuration")
data: Dict[str, Any] = Field(description="Resource data, eg: container liquid data")
extra: Dict[str, Any] = Field(description="Extra data, eg: slot index")
data: Dict[str, Any] = Field(description="Resource data")
extra: Dict[str, Any] = Field(description="Extra data")
@field_serializer("parent_uuid")
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
@@ -155,7 +155,7 @@ class ResourceDictInstance(object):
res_dict = self.res_content.model_dump(by_alias=True)
res_dict["children"] = {child.res_content.id: child.get_plr_nested_dict() for child in self.children}
res_dict["parent"] = self.res_content.parent_instance_name
res_dict["position"] = self.res_content.pose.position.model_dump()
res_dict["position"] = self.res_content.position.position.model_dump()
del res_dict["pose"]
return res_dict

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
from ast import If
import pytest
import json
import os
@@ -9,16 +8,18 @@ from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.registry.registry import lab_registry
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
from unilabos.resources.bioyond.decks import YB_Deck
lab_registry.setup()
type_mapping = {
"加样头(大)": ("YB_jia_yang_tou_da", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
"": ("YB_1BottleCarrier", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
"配液瓶(小)": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
"配液瓶(小)": ("YB_pei_ye_xiao_Bottler", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
"样品": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
}
@@ -56,20 +57,12 @@ def bioyond_materials_liquidhandling_2() -> list[dict]:
"bioyond_materials_reaction",
"bioyond_materials_liquidhandling_1",
])
def test_resourcetreeset_from_plr() -> list[dict]:
# 直接加载 bioyond_materials_reaction.json 文件
current_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(current_dir, "test.json")
with open(json_path, "r", encoding="utf-8") as f:
materials = json.load(f)
deck = YB_Deck("test_deck")
def test_resourcetreeset_from_plr(materials_fixture, request) -> list[dict]:
materials = request.getfixturevalue(materials_fixture)
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
print(output)
# print(deck.summary())
print(deck.summary())
r = ResourceTreeSet.from_plr_resources([deck])
print(r.dump())
# json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)
if __name__ == "__main__":
test_resourcetreeset_from_plr()

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -0,0 +1,94 @@
import json
import sys
from datetime import datetime
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
import pytest
from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
def _normalize_steps(data):
normalized = []
for step in data:
action = step.get("action") or step.get("operation")
if not action:
continue
raw_params = step.get("parameters") or step.get("action_args") or {}
params = dict(raw_params)
if "source" in raw_params and "sources" not in raw_params:
params["sources"] = raw_params["source"]
if "target" in raw_params and "targets" not in raw_params:
params["targets"] = raw_params["target"]
description = step.get("description") or step.get("purpose")
step_dict = {"action": action, "parameters": params}
if description:
step_dict["description"] = description
normalized.append(step_dict)
return normalized
def _normalize_labware(data):
labware = {}
for item in data:
reagent_name = item.get("reagent_name")
key = reagent_name or item.get("material_name") or item.get("name")
if not key:
continue
key = str(key)
idx = 1
original_key = key
while key in labware:
idx += 1
key = f"{original_key}_{idx}"
labware[key] = {
"slot": item.get("positions") or item.get("slot"),
"labware": item.get("material_name") or item.get("labware"),
"well": item.get("well", []),
"type": item.get("type", "reagent"),
"role": item.get("role", ""),
"name": key,
}
return labware
@pytest.mark.parametrize("protocol_name", [
"example_bio",
# "bioyond_materials_liquidhandling_1",
"example_prcxi",
])
def test_build_protocol_graph(protocol_name):
data_path = Path(__file__).with_name(f"{protocol_name}.json")
with data_path.open("r", encoding="utf-8") as fp:
d = json.load(fp)
if "workflow" in d and "reagent" in d:
protocol_steps = d["workflow"]
labware_info = d["reagent"]
elif "steps_info" in d and "labware_info" in d:
protocol_steps = _normalize_steps(d["steps_info"])
labware_info = _normalize_labware(d["labware_info"])
else:
raise ValueError("Unsupported protocol format")
graph = build_protocol_graph(
labware_info=labware_info,
protocol_steps=protocol_steps,
workstation_name="PRCXi",
)
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
output_path = data_path.with_name(f"{protocol_name}_graph_{timestamp}.png")
draw_protocol_graph_with_ports(graph, str(output_path))
print(graph)

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