Compare commits

..

35 Commits

Author SHA1 Message Date
Xuwznln
638bff5bab Revert opcua client & move electrolyte node 2026-01-17 16:49:52 +08:00
Xuwznln
50a5086ba5 Revert log change & update registry 2026-01-17 16:48:54 +08:00
Xuwznln
4c8022ee95 Workstation yb merge dev ready 260113 (#216)
* feat(bioyond): 添加计算实验设计功能,支持化合物配比和滴定比例参数

* feat(bioyond): 添加测量小瓶功能,支持基本参数配置

* feat(bioyond): 添加测量小瓶配置,支持新设备参数

* feat(bioyond): 更新仓库布局和尺寸,支持竖向排列的测量小瓶和试剂存放堆栈

* feat(bioyond): 优化任务创建流程,确保无论成功与否都清理任务队列以避免重复累积

* feat(bioyond): 添加设置反应器温度功能,支持温度范围和异常处理

* feat(bioyond): 调整反应器位置配置,统一坐标格式

* feat(bioyond): 添加调度器启动功能,支持任务队列执行并处理异常

* feat(bioyond): 优化调度器启动功能,添加异常处理并更新相关配置

* feat(opcua): 增强节点ID解析兼容性和数据类型处理

改进节点ID解析逻辑以支持多种格式,包括字符串和数字标识符
添加数据类型转换处理,确保写入值时类型匹配
优化错误提示信息,便于调试节点连接问题

* feat(registry): 新增后处理站的设备配置文件

添加后处理站的YAML配置文件,包含动作映射、状态类型和设备描述

* 添加调度器启动功能,合并物料参数配置,优化物料参数处理逻辑

* 添加从 Bioyond 系统自动同步工作流序列的功能,并更新相关配置

* fix:兼容 BioyondReactionStation 中 workflow_sequence 被重写为 property

* fix:同步工作流序列

* feat: remove commented workflow synchronization from `reaction_station.py`.

* 添加时间约束功能及相关配置

* fix:自动更新物料缓存功能,添加物料时更新缓存并在删除时移除缓存项

* fix:在添加物料时处理字符串和字典返回值,确保正确更新缓存

* fix:更新奔曜错误处理报送为物料变更报送,调整日志记录和响应消息

* feat:添加实验报告简化功能,去除冗余信息并保留关键信息

* feat: 添加任务状态事件发布功能,监控并报告任务运行、超时、完成和错误状态

* fix: 修复添加物料时数据格式错误

* Refactor bioyond_dispensing_station and reaction_station_bioyond YAML configurations

- Removed redundant action value mappings from bioyond_dispensing_station.
- Updated goal properties in bioyond_dispensing_station to use enums for target_stack and other parameters.
- Changed data types for end_point and start_point in reaction_station_bioyond to use string enums (Start, End).
- Simplified descriptions and updated measurement units from μL to mL where applicable.
- Removed unused commands from reaction_station_bioyond to streamline the configuration.

* fix:Change the material unit from μL to mL

* fix:refresh_material_cache

* feat: 动态获取工作流步骤ID,优化工作流配置

* feat: 添加清空服务端所有非核心工作流功能

* fix:修复Bottle类的序列化和反序列化方法

* feat:增强材料缓存更新逻辑,支持处理返回数据中的详细信息

* Add debug log

* feat(workstation): update bioyond config migration and coin cell material search logic

- Migrate bioyond_cell config to JSON structure and remove global variable dependencies
- Implement material search confirmation dialog auto-handling
- Add documentation: 20260113_物料搜寻确认弹窗自动处理功能.md and 20260113_配置迁移修改总结.md

* Refactor module paths for Bioyond devices in YAML configuration files

- Updated the module path for BioyondDispensingStation in bioyond_dispensing_station.yaml to reflect the new directory structure.
- Updated the module path for BioyondReactionStation and BioyondReactor in reaction_station_bioyond.yaml to align with the revised organization of the codebase.

* fix: WareHouse 的不可哈希类型错误,优化父节点去重逻辑

* refactor: Move config from module to instance initialization

* fix: 修正 reaction_station 目录名拼写错误

* feat: Integrate material search logic and cleanup deprecated files

- Update coin_cell_assembly.py with material search dialog handling
- Update YB_warehouses.py with latest warehouse configurations
- Remove outdated documentation and test data files

* Refactor: Use instance attributes for action names and workflow step IDs

* refactor: Split tipbox storage into left and right warehouses

* refactor: Merge tipbox storage left and right into single warehouse

---------

Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
Co-authored-by: Andy6M <xieqiming1132@qq.com>
2026-01-17 15:44:18 +08:00
ZiWei
ad21644db0 fix: WareHouse 的不可哈希类型错误,优化父节点去重逻辑 2026-01-14 20:15:05 +08:00
Xuwznln
9dfd58e9af fix parent_uuid fetch when bind_parent_id == node_name 2026-01-14 14:17:29 +08:00
Xuwznln
31c9f9a172 物料更新也是用父节点进行报送 2026-01-13 20:21:37 +08:00
Xuwznln
02cd8de4c5 Add None conversion for tube rack etc. 2026-01-13 17:49:11 +08:00
Xuwznln
a66603ec1c Add set_liquid example. 2026-01-12 22:24:01 +08:00
Xuwznln
ec015e16cd Add create_resource and test_resource example. 2026-01-12 21:17:28 +08:00
Xuwznln
965bf36e8d Add restart.
Temp allow action message.
2026-01-11 21:25:59 +08:00
Xuwznln
aacf3497e0 Add no_update_feedback option. 2026-01-09 17:18:39 +08:00
Xuwznln
657f952e7a Create session_id by edge. 2026-01-09 12:01:57 +08:00
Xuwznln
0165590290 bump version to 0.10.15 2026-01-08 15:37:49 +08:00
Xuwznln
daea1ab54d temp cancel update req 2026-01-08 15:26:31 +08:00
Xuwznln
93cb307396 Fix update with different spot and same parent 2026-01-08 03:46:00 +08:00
Xuwznln
1c312772ae Force update resource when adding new resource / transfer to another resource 2026-01-08 03:07:12 +08:00
Xuwznln
bad1db5094 location not passed to ItemizedCarrier when assign child resource 2026-01-08 03:07:11 +08:00
Xuwznln
f26eb69eca Fix size not pass through. 2026-01-08 03:07:11 +08:00
Xuwznln
12c0770c92 Fix build on macos-intel 2026-01-07 21:11:10 +08:00
Xuwznln
3d2d428a96 Update README.md
Modify resource_tracker file module path.

(cherry picked from commit 8066c200b9)
2026-01-07 20:54:43 +08:00
Xuwznln
78bf57f590 Bump version to 0.10.4 2026-01-07 20:41:23 +08:00
Xuwznln
e227cddab3 Update LICENSE 2026-01-07 20:40:02 +08:00
Xuwznln
f2b993643f Fix drag materials. 2026-01-07 19:40:29 +08:00
Xuwznln
2e14bf197c Fix and tested new create_resource. 2026-01-07 19:26:42 +08:00
Xuwznln
66c18c080a Update create_resource to resource tree mode. 2026-01-07 02:03:43 +08:00
Xuwznln
a1c34f138e Close #208. Fix mock devices.
(cherry picked from commit 28f93737ac)
2025-12-28 23:24:44 +08:00
Xianwei Qi
75bb5ec553 test_transfer_liquid_2 2025-12-26 16:42:50 +08:00
Xianwei Qi
bb95c89829 Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-12-26 16:25:19 +08:00
Xianwei Qi
394c140830 test_transfer_liquid 2025-12-26 16:24:55 +08:00
Xuwznln
e6d8d41183 bump version to 0.10.3 2025-12-26 03:26:50 +08:00
Xuwznln
847a300af3 update registry 2025-12-26 03:26:46 +08:00
Xuwznln
a201d7c307 update registry 2025-12-26 03:26:45 +08:00
Xuwznln
3433766bc5 do not modify globally 2025-12-26 03:26:44 +08:00
Xuwznln
7e9e93b29c Prcix9320 (#207)
* 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>

* fix startup env check.
add auto install during one-key installation

* Try fix one-key build on linux

* Complete all one key installation

* fix: rename schema field to resource_schema with serialization and validation aliases (#104)

Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>

* 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

* Fix conda pack on windows

* add plr_to_bioyond, and refactor bioyond stations

* modify default config

* Fix one-key installation build for windows

* Fix workstation startup
Update registry

* 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>

* Close #107
Update doc url.

* 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>

* fix resource_get in action

* fix(reaction_station): 清空工作流序列和参数避免重复执行 (#113)

在创建任务后清空工作流序列和参数,防止下次执行时累积重复

* Update create_resource device_id

* Update ResourceTracker

add more enumeration in POSE

fix converter in resource_tracker

* 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.

* 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调用,以符合接口设计规范

* PRCXI Update

修改prcxi连线

prcxi样例图

Create example_prcxi.json

* 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

* 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%小瓶投料任务和二胺溶液配置任务的功能,更新相关参数和默认值

* 封膜仪、撕膜仪、耗材站接口

* 添加Raman和xrd相关代码

* 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

* bump version to 0.10.9

update registry

* 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 属性

* 1114物料手册定义教程byxinyu (#165)

* 宜宾奔耀工站deck前端by_Xinyu

* 构建物料教程byxinyu

* 1114物料手册定义教程

* 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>

* 标准化opcua设备接入unilab (#78)

* 初始提交,只保留工作区当前状态

* remove redundant arm_slider meshes

---------

Co-authored-by: Junhan Chang <changjh@dp.tech>

* add new laiyu liquid driver, yaml and json files (#164)

* 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>

* nmr

* Update devices

* bump version to 0.10.10

* Update repo files.

* Add get_resource_with_dir & get_resource method

* fix camera & workstation & warehouse & reaction station driver

* update docs, test examples
fix liquid_handler init bug

* bump version to 0.10.11

* Add startup_json_path, disable_browser, port config

* Update oss config

* feat(bioyond_studio): 添加项目API接口支持及优化物料管理功能

添加通用项目API接口方法(_post_project_api, _delete_project_api)用于与LIMS系统交互
实现compute_experiment_design方法用于实验设计计算
新增brief_step_parameters等订单相关接口方法
优化物料转移逻辑,增加异步任务处理
扩展BioyondV1RPC类,添加批量物料操作、订单状态管理等功能

* feat(bioyond): 添加测量小瓶仓库和更新仓库工厂函数参数

* Support unilabos_samples key

* add session_id and normal_exit

* Add result schema and add TypedDict conversion.

* Fix port error

* Add backend api and update doc

* Add get_regular_container func

* Add get_regular_container func

* 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.

* Auto dump logs, fix workstation input schema

* 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 图片

* signal when host node is ready

* fix ros2 future

print all logs to file
fix resource dict dump error

* update version to 0.10.12

* 修改sample_uuid的返回值

* 修改pose标签设定机制

* 添加 aspiate函数返回值

* 返回dispense后的sample_uuid

* 添加self.pending_liquids_dict的重置方法

* 修改prcxi的json文件,解决trach错误问题

* 修改prcxijson,防止PlateT4的硬件错误

* 对laiyu移液站进行部分修改,取消多次初始化的问题

* 修改根据新的物料格式,修改可视化

* 添加切换枪头方法,添加mock振荡与加热方法

* 夹爪添加

* 删除多余的laiyu部分

* 云端可启动夹爪

* Delete __init__.py

* Enhance PRCXI9300 classes with new Container and TipRack implementations, improving state management and initialization logic. Update JSON configuration to reflect type changes for containers and plates.

* 修改上传数据

---------

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>
Co-authored-by: hh.(SII) <103566763+Mile-Away@users.noreply.github.com>
Co-authored-by: Xianwei Qi <qxw@stu.pku.edu.cn>
Co-authored-by: WenzheG <wenzheguo32@gmail.com>
Co-authored-by: Harry Liu <113173203+ALITTLELZ@users.noreply.github.com>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: tt <166512503+tt11142023@users.noreply.github.com>
Co-authored-by: xyc <49015816+xiaoyu10031@users.noreply.github.com>
Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: zhangshixiang <554662886@qq.com>
Co-authored-by: ALITTLELZ <l_LZlz@163.com>
2025-12-26 02:28:56 +08:00
Xuwznln
9e1e6da505 Add topic config 2025-12-26 02:26:17 +08:00
162 changed files with 99091 additions and 102518 deletions

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: 0.10.12
version: 0.10.15
source:
path: ../unilabos
@@ -9,7 +9,7 @@ source:
build:
python:
entry_points:
- unilab = unilabos.app.main:main
- unilab = unilabos.app.main:main
script:
- set PIP_NO_INDEX=
- if: win
@@ -25,7 +25,6 @@ build:
- cp $RECIPE_DIR/../setup.py $SRC_DIR
- $PYTHON -m pip install $SRC_DIR
requirements:
host:
- python ==3.11.11
@@ -87,6 +86,6 @@ requirements:
- uni-lab::ros-humble-unilabos-msgs
about:
repository: https://github.com/dptech-corp/Uni-Lab-OS
repository: https://github.com/deepmodeling/Uni-Lab-OS
license: GPL-3.0-only
description: "Uni-Lab-OS"

26
.cursorignore Normal file
View File

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

View File

@@ -24,7 +24,7 @@ jobs:
platform: linux-64
env_file: unilabos-linux-64.yaml
script_ext: sh
- os: macos-13 # Intel
- os: macos-15 # Intel (via Rosetta)
platform: osx-64
env_file: unilabos-osx-64.yaml
script_ext: sh

View File

@@ -27,7 +27,7 @@ jobs:
- os: ubuntu-latest
platform: linux-64
env_file: unilabos-linux-64.yaml
- os: macos-13 # Intel
- os: macos-15 # Intel (via Rosetta)
platform: osx-64
env_file: unilabos-osx-64.yaml
- os: macos-latest # ARM64

View File

@@ -26,7 +26,7 @@ jobs:
include:
- os: ubuntu-latest
platform: linux-64
- os: macos-13 # Intel
- os: macos-15 # Intel (via Rosetta)
platform: osx-64
- os: macos-latest # ARM64
platform: osx-arm64

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
cursor_docs/
configs/
temp/
output/

17
NOTICE Normal file
View File

@@ -0,0 +1,17 @@
# Uni-Lab-OS Licensing Notice
This project uses a dual licensing structure:
## 1. Main Framework - GPL-3.0
- unilabos/ (except unilabos/devices/)
- docs/
- tests/
See [LICENSE](LICENSE) for details.
## 2. Device Drivers - DP Technology Proprietary License
- unilabos/devices/
See [unilabos/devices/LICENSE](unilabos/devices/LICENSE) for details.

View File

@@ -8,17 +8,13 @@
**English** | [中文](README_zh.md)
[![GitHub Stars](https://img.shields.io/github/stars/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/network/members)
[![GitHub Issues](https://img.shields.io/github/issues/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/issues)
[![GitHub License](https://img.shields.io/github/license/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
[![GitHub Stars](https://img.shields.io/github/stars/dptech-corp/Uni-Lab-OS.svg)](https://github.com/deepmodeling/Uni-Lab-OS/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/dptech-corp/Uni-Lab-OS.svg)](https://github.com/deepmodeling/Uni-Lab-OS/network/members)
[![GitHub Issues](https://img.shields.io/github/issues/dptech-corp/Uni-Lab-OS.svg)](https://github.com/deepmodeling/Uni-Lab-OS/issues)
[![GitHub License](https://img.shields.io/github/license/dptech-corp/Uni-Lab-OS.svg)](https://github.com/deepmodeling/Uni-Lab-OS/blob/main/LICENSE)
Uni-Lab-OS is a platform for laboratory automation, designed to connect and control various experimental equipment, enabling automation and standardization of experimental workflows.
## 🏆 Competition
Join the [Intelligent Organic Chemistry Synthesis Competition](https://bohrium.dp.tech/competitions/1451645258) to explore automated synthesis with Uni-Lab-OS!
## Key Features
- Multi-device integration management
@@ -31,11 +27,13 @@ Join the [Intelligent Organic Chemistry Synthesis Competition](https://bohrium.d
Detailed documentation can be found at:
- [Online Documentation](https://xuwznln.github.io/Uni-Lab-OS-Doc/)
- [Online Documentation](https://deepmodeling.github.io/Uni-Lab-OS/)
## Quick Start
Uni-Lab-OS recommends using `mamba` for environment management. Choose the appropriate environment file for your operating system:
1. Setup Conda Environment
Uni-Lab-OS recommends using `mamba` for environment management:
```bash
# Create new environment
@@ -44,28 +42,54 @@ mamba activate unilab
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
```
## Install Dev Uni-Lab-OS
2. Install Dev Uni-Lab-OS
```bash
# Clone the repository
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
cd Uni-Lab-OS
# Install Uni-Lab-OS
pip install .
```
3. Start Uni-Lab System:
3. Start Uni-Lab System
Please refer to [Documentation - Boot Examples](https://xuwznln.github.io/Uni-Lab-OS-Doc/boot_examples/index.html)
Please refer to [Documentation - Boot Examples](https://deepmodeling.github.io/Uni-Lab-OS/boot_examples/index.html)
4. Best Practice
See [Best Practice Guide](https://deepmodeling.github.io/Uni-Lab-OS/user_guide/best_practice.html)
## Message Format
Uni-Lab-OS uses pre-built `unilabos_msgs` for system communication. You can find the built versions on the [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) page.
Uni-Lab-OS uses pre-built `unilabos_msgs` for system communication. You can find the built versions on the [GitHub Releases](https://github.com/deepmodeling/Uni-Lab-OS/releases) page.
## Citation
If you use [Uni-Lab-OS](https://arxiv.org/abs/2512.21766) in academic research, please cite:
```bibtex
@article{gao2025unilabos,
title = {UniLabOS: An AI-Native Operating System for Autonomous Laboratories},
doi = {10.48550/arXiv.2512.21766},
publisher = {arXiv},
author = {Gao, Jing and Chang, Junhan and Que, Haohui and Xiong, Yanfei and
Zhang, Shixiang and Qi, Xianwei and Liu, Zhen and Wang, Jun-Jie and
Ding, Qianjun and Li, Xinyu and Pan, Ziwei and Xie, Qiming and
Yan, Zhuang and Yan, Junchi and Zhang, Linfeng},
year = {2025}
}
```
## License
This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for details.
This project uses a dual licensing structure:
- **Main Framework**: GPL-3.0 - see [LICENSE](LICENSE)
- **Device Drivers** (`unilabos/devices/`): DP Technology Proprietary License
See [NOTICE](NOTICE) for complete licensing details.
## Project Statistics
@@ -77,4 +101,4 @@ This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for det
## Contact Us
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
- GitHub Issues: [https://github.com/deepmodeling/Uni-Lab-OS/issues](https://github.com/deepmodeling/Uni-Lab-OS/issues)

View File

@@ -8,17 +8,13 @@
[English](README.md) | **中文**
[![GitHub Stars](https://img.shields.io/github/stars/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/network/members)
[![GitHub Issues](https://img.shields.io/github/issues/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/issues)
[![GitHub License](https://img.shields.io/github/license/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
[![GitHub Stars](https://img.shields.io/github/stars/dptech-corp/Uni-Lab-OS.svg)](https://github.com/deepmodeling/Uni-Lab-OS/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/dptech-corp/Uni-Lab-OS.svg)](https://github.com/deepmodeling/Uni-Lab-OS/network/members)
[![GitHub Issues](https://img.shields.io/github/issues/dptech-corp/Uni-Lab-OS.svg)](https://github.com/deepmodeling/Uni-Lab-OS/issues)
[![GitHub License](https://img.shields.io/github/license/dptech-corp/Uni-Lab-OS.svg)](https://github.com/deepmodeling/Uni-Lab-OS/blob/main/LICENSE)
Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
## 🏆 比赛
欢迎参加[有机化学合成智能实验大赛](https://bohrium.dp.tech/competitions/1451645258),使用 Uni-Lab-OS 探索自动化合成!
## 核心特点
- 多设备集成管理
@@ -31,7 +27,7 @@ Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控
详细文档可在以下位置找到:
- [在线文档](https://xuwznln.github.io/Uni-Lab-OS-Doc/)
- [在线文档](https://deepmodeling.github.io/Uni-Lab-OS/)
## 快速开始
@@ -50,24 +46,50 @@ mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
```bash
# 克隆仓库
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
cd Uni-Lab-OS
# 安装 Uni-Lab-OS
pip install .
```
3. 启动 Uni-Lab 系统:
3. 启动 Uni-Lab 系统
请见[文档-启动样例](https://xuwznln.github.io/Uni-Lab-OS-Doc/boot_examples/index.html)
请见[文档-启动样例](https://deepmodeling.github.io/Uni-Lab-OS/boot_examples/index.html)
4. 最佳实践
请见[最佳实践指南](https://deepmodeling.github.io/Uni-Lab-OS/user_guide/best_practice.html)
## 消息格式
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) 页面找到已构建的版本。
Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/deepmodeling/Uni-Lab-OS/releases) 页面找到已构建的版本。
## 引用
如果您在学术研究中使用 [Uni-Lab-OS](https://arxiv.org/abs/2512.21766),请引用:
```bibtex
@article{gao2025unilabos,
title = {UniLabOS: An AI-Native Operating System for Autonomous Laboratories},
doi = {10.48550/arXiv.2512.21766},
publisher = {arXiv},
author = {Gao, Jing and Chang, Junhan and Que, Haohui and Xiong, Yanfei and
Zhang, Shixiang and Qi, Xianwei and Liu, Zhen and Wang, Jun-Jie and
Ding, Qianjun and Li, Xinyu and Pan, Ziwei and Xie, Qiming and
Yan, Zhuang and Yan, Junchi and Zhang, Linfeng},
year = {2025}
}
```
## 许可证
项目采用 GPL-3.0 许可 - 详情请参阅 [LICENSE](LICENSE) 文件。
项目采用双许可证结构:
- **主框架**GPL-3.0 - 详见 [LICENSE](LICENSE)
- **设备驱动** (`unilabos/devices/`):深势科技专有许可证
完整许可证说明请参阅 [NOTICE](NOTICE)。
## 项目统计
@@ -79,4 +101,4 @@ Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在
## 联系我们
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
- GitHub Issues: [https://github.com/deepmodeling/Uni-Lab-OS/issues](https://github.com/deepmodeling/Uni-Lab-OS/issues)

View File

@@ -24,7 +24,7 @@ extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
"sphinx_rtd_theme",
"sphinxcontrib.mermaid"
"sphinxcontrib.mermaid",
]
source_suffix = {
@@ -58,7 +58,7 @@ html_theme = "sphinx_rtd_theme"
# sphinx-book-theme 主题选项
html_theme_options = {
"repository_url": "https://github.com/用户名/Uni-Lab",
"repository_url": "https://github.com/deepmodeling/Uni-Lab-OS",
"use_repository_button": True,
"use_issues_button": True,
"use_edit_page_button": True,

File diff suppressed because it is too large Load Diff

View File

@@ -12,3 +12,7 @@ sphinx-copybutton>=0.5.0
# 用于自动摘要生成
sphinx-autobuild>=2024.2.4
# 用于PDF导出 (rinohtype方案纯Python无需LaTeX)
rinohtype>=0.5.4
sphinx-simplepdf>=1.6.0

View File

@@ -1807,7 +1807,7 @@ unilab --ak your_ak --sk your_sk -g graph.json \
#### 14.5 社区支持
- **GitHub Issues**[https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
- **GitHub Issues**[https://github.com/deepmodeling/Uni-Lab-OS/issues](https://github.com/deepmodeling/Uni-Lab-OS/issues)
- **官方网站**[https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
---

View File

@@ -463,7 +463,7 @@ Uni-Lab 使用 `ResourceDictInstance.get_resource_instance_from_dict()` 方法
### 使用示例
```python
from unilabos.ros.nodes.resource_tracker import ResourceDictInstance
from unilabos.resources.resource_tracker import ResourceDictInstance
# 旧格式节点
old_format_node = {
@@ -477,10 +477,10 @@ old_format_node = {
instance = ResourceDictInstance.get_resource_instance_from_dict(old_format_node)
# 访问标准化后的数据
print(instance.res_content.id) # "pump_1"
print(instance.res_content.uuid) # 自动生成的 UUID
print(instance.res_content.id) # "pump_1"
print(instance.res_content.uuid) # 自动生成的 UUID
print(instance.res_content.config) # {}
print(instance.res_content.data) # {}
print(instance.res_content.data) # {}
```
### 格式迁移建议
@@ -857,4 +857,4 @@ class ResourceDictPosition(BaseModel):
- 在 Web 界面中使用模板创建
- 参考示例文件:`test/experiments/` 目录
- 查看 ResourceDict 源码了解完整定义
- [GitHub 讨论区](https://github.com/dptech-corp/Uni-Lab-OS/discussions)
- [GitHub 讨论区](https://github.com/deepmodeling/Uni-Lab-OS/discussions)

View File

@@ -37,7 +37,7 @@
#### 第一步:下载预打包环境
1. 访问 [GitHub Actions - Conda Pack Build](https://github.com/dptech-corp/Uni-Lab-OS/actions/workflows/conda-pack-build.yml)
1. 访问 [GitHub Actions - Conda Pack Build](https://github.com/deepmodeling/Uni-Lab-OS/actions/workflows/conda-pack-build.yml)
2. 选择最新的成功构建记录(绿色勾号 ✓)
@@ -189,13 +189,13 @@ conda activate unilab
### 第一步:克隆仓库
```bash
git clone https://github.com/dptech-corp/Uni-Lab-OS.git
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
cd Uni-Lab-OS
```
如果您需要贡献代码,建议先 Fork 仓库:
1. 访问 https://github.com/dptech-corp/Uni-Lab-OS
1. 访问 https://github.com/deepmodeling/Uni-Lab-OS
2. 点击右上角的 "Fork" 按钮
3. Clone 您的 Fork 版本:
```bash
@@ -240,7 +240,7 @@ pip uninstall unilabos -y
# 克隆 dev 分支(如果还未克隆)
cd /path/to/your/workspace
git clone -b dev https://github.com/dptech-corp/Uni-Lab-OS.git
git clone -b dev https://github.com/deepmodeling/Uni-Lab-OS.git
# 或者如果已经克隆,切换到 dev 分支
cd Uni-Lab-OS
git checkout dev
@@ -503,9 +503,9 @@ mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-f
## 需要帮助?
- **故障排查**: 查看更详细的故障排查信息
- **GitHub Issues**: [报告问题](https://github.com/dptech-corp/Uni-Lab-OS/issues)
- **GitHub Issues**: [报告问题](https://github.com/deepmodeling/Uni-Lab-OS/issues)
- **开发者文档**: 查看开发者指南获取更多技术细节
- **社区讨论**: [GitHub Discussions](https://github.com/dptech-corp/Uni-Lab-OS/discussions)
- **社区讨论**: [GitHub Discussions](https://github.com/deepmodeling/Uni-Lab-OS/discussions)
---

View File

@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
version: 0.10.12
version: 0.10.15
source:
path: ../../unilabos_msgs
target_directory: src
@@ -17,7 +17,7 @@ build:
- bash $SRC_DIR/build_ament_cmake.sh
about:
repository: https://github.com/dptech-corp/Uni-Lab-OS
repository: https://github.com/deepmodeling/Uni-Lab-OS
license: BSD-3-Clause
description: "ros-humble-unilabos-msgs is a package that provides message definitions for Uni-Lab-OS."

View File

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

View File

@@ -126,7 +126,7 @@ If installation fails:
For more help:
- Documentation: docs/user_guide/installation.md
- Quick Start: QUICK_START_CONDA_PACK.md
- Issues: https://github.com/dptech-corp/Uni-Lab-OS/issues
- Issues: https://github.com/deepmodeling/Uni-Lab-OS/issues
License:
--------
@@ -134,7 +134,7 @@ License:
UniLabOS is licensed under GPL-3.0-only.
See LICENSE file for details.
Repository: https://github.com/dptech-corp/Uni-Lab-OS
Repository: https://github.com/deepmodeling/Uni-Lab-OS
"""
return readme

View File

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

7
tests/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
"""
测试包根目录。
让 `tests.*` 模块可以被正常 import例如给 `unilabos` 下的测试入口使用)。
"""

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,5 @@
"""
液体处理设备相关测试。
"""

View File

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

View File

@@ -2,9 +2,8 @@ import pytest
import json
import os
from pylabrobot.resources import Resource as ResourcePLR
from unilabos.resources.graphio import resource_bioyond_to_plr
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.resources.resource_tracker import ResourceTreeSet
from unilabos.registry.registry import lab_registry
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck

View File

@@ -11,10 +11,10 @@ import os
# 添加项目根目录到路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
# 导入测试模块
from test.ros.msgs.test_basic import TestBasicFunctionality
from test.ros.msgs.test_conversion import TestBasicConversion, TestMappingConversion
from test.ros.msgs.test_mapping import TestTypeMapping, TestFieldMapping
# 导入测试模块(统一从 tests 包获取)
from tests.ros.msgs.test_basic import TestBasicFunctionality
from tests.ros.msgs.test_conversion import TestBasicConversion, TestMappingConversion
from tests.ros.msgs.test_mapping import TestTypeMapping, TestFieldMapping
def run_tests():

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

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

View File

@@ -1,6 +1,6 @@
import threading
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.resources.resource_tracker import ResourceTreeSet
from unilabos.utils import logger

View File

@@ -19,6 +19,11 @@ 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
from unilabos.app.utils import cleanup_for_restart
# Global restart flags (used by ws_client and web/server)
_restart_requested: bool = False
_restart_reason: str = ""
def load_config_from_file(config_path):
@@ -156,6 +161,11 @@ def parse_args():
default=False,
help="Complete registry information",
)
parser.add_argument(
"--no_update_feedback",
action="store_true",
help="Disable sending update feedback to server",
)
# workflow upload subcommand
workflow_parser = subparsers.add_parser(
"workflow_upload",
@@ -297,6 +307,7 @@ def main():
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)
BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False)
BasicConfig.communication_protocol = "websocket"
machine_name = os.popen("hostname").read().strip()
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
@@ -315,7 +326,7 @@ def main():
from unilabos.app.web import start_server
from unilabos.app.register import register_devices_and_resources
from unilabos.resources.graphio import modify_to_backend_format
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
# 显示启动横幅
print_unilab_banner(args_dict)
@@ -418,7 +429,7 @@ def main():
# 如果从远端获取了物料信息,则与本地物料进行同步
if request_startup_json and "nodes" in request_startup_json:
print_status("开始同步远端物料到本地...", "info")
remote_tree_set = ResourceTreeSet.from_raw_list(request_startup_json["nodes"])
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
resource_tree_set.merge_remote_resources(remote_tree_set)
print_status("远端物料同步完成", "info")
@@ -497,13 +508,19 @@ def main():
time.sleep(1)
else:
start_backend(**args_dict)
start_server(
restart_requested = start_server(
open_browser=not args_dict["disable_browser"],
port=BasicConfig.port,
)
if restart_requested:
print_status("[Main] Restart requested, cleaning up...", "info")
cleanup_for_restart()
return
else:
start_backend(**args_dict)
start_server(
# 启动服务器默认支持WebSocket触发重启
restart_requested = start_server(
open_browser=not args_dict["disable_browser"],
port=BasicConfig.port,
)

144
unilabos/app/utils.py Normal file
View File

@@ -0,0 +1,144 @@
"""
UniLabOS 应用工具函数
提供清理、重启等工具函数
"""
import gc
import os
import threading
import time
from unilabos.utils.banner_print import print_status
def cleanup_for_restart() -> bool:
"""
Clean up all resources for restart without exiting the process.
This function prepares the system for re-initialization by:
1. Stopping all communication clients
2. Destroying ROS nodes
3. Resetting singletons
4. Waiting for threads to finish
Returns:
bool: True if cleanup was successful, False otherwise
"""
print_status("[Restart] Starting cleanup for restart...", "info")
# Step 1: Stop WebSocket communication client
print_status("[Restart] Step 1: Stopping WebSocket client...", "info")
try:
from unilabos.app.communication import get_communication_client
comm_client = get_communication_client()
if comm_client is not None:
comm_client.stop()
print_status("[Restart] WebSocket client stopped", "info")
except Exception as e:
print_status(f"[Restart] Error stopping WebSocket: {e}", "warning")
# Step 2: Get HostNode and cleanup ROS
print_status("[Restart] Step 2: Cleaning up ROS nodes...", "info")
try:
from unilabos.ros.nodes.presets.host_node import HostNode
import rclpy
from rclpy.timer import Timer
host_instance = HostNode.get_instance(timeout=5)
if host_instance is not None:
print_status(f"[Restart] Found HostNode: {host_instance.device_id}", "info")
# Gracefully shutdown background threads
print_status("[Restart] Shutting down background threads...", "info")
HostNode.shutdown_background_threads(timeout=5.0)
print_status("[Restart] Background threads shutdown complete", "info")
# Stop discovery timer
if hasattr(host_instance, "_discovery_timer") and isinstance(host_instance._discovery_timer, Timer):
host_instance._discovery_timer.cancel()
print_status("[Restart] Discovery timer cancelled", "info")
# Destroy device nodes
device_count = len(host_instance.devices_instances)
print_status(f"[Restart] Destroying {device_count} device instances...", "info")
for device_id, device_node in list(host_instance.devices_instances.items()):
try:
if hasattr(device_node, "ros_node_instance") and device_node.ros_node_instance is not None:
device_node.ros_node_instance.destroy_node()
print_status(f"[Restart] Device {device_id} destroyed", "info")
except Exception as e:
print_status(f"[Restart] Error destroying device {device_id}: {e}", "warning")
# Clear devices instances
host_instance.devices_instances.clear()
host_instance.devices_names.clear()
# Destroy host node
try:
host_instance.destroy_node()
print_status("[Restart] HostNode destroyed", "info")
except Exception as e:
print_status(f"[Restart] Error destroying HostNode: {e}", "warning")
# Reset HostNode state
HostNode.reset_state()
print_status("[Restart] HostNode state reset", "info")
# Shutdown executor first (to stop executor.spin() gracefully)
if hasattr(rclpy, "__executor") and rclpy.__executor is not None:
try:
rclpy.__executor.shutdown()
rclpy.__executor = None # Clear for restart
print_status("[Restart] ROS executor shutdown complete", "info")
except Exception as e:
print_status(f"[Restart] Error shutting down executor: {e}", "warning")
# Shutdown rclpy
if rclpy.ok():
rclpy.shutdown()
print_status("[Restart] rclpy shutdown complete", "info")
except ImportError as e:
print_status(f"[Restart] ROS modules not available: {e}", "warning")
except Exception as e:
print_status(f"[Restart] Error in ROS cleanup: {e}", "warning")
return False
# Step 3: Reset communication client singleton
print_status("[Restart] Step 3: Resetting singletons...", "info")
try:
from unilabos.app import communication
if hasattr(communication, "_communication_client"):
communication._communication_client = None
print_status("[Restart] Communication client singleton reset", "info")
except Exception as e:
print_status(f"[Restart] Error resetting communication singleton: {e}", "warning")
# Step 4: Wait for threads to finish
print_status("[Restart] Step 4: Waiting for threads to finish...", "info")
time.sleep(3) # Give threads time to finish
# Check remaining threads
remaining_threads = []
for t in threading.enumerate():
if t.name != "MainThread" and t.is_alive():
remaining_threads.append(t.name)
if remaining_threads:
print_status(
f"[Restart] Warning: {len(remaining_threads)} threads still running: {remaining_threads}", "warning"
)
else:
print_status("[Restart] All threads stopped", "info")
# Step 5: Force garbage collection
print_status("[Restart] Step 5: Running garbage collection...", "info")
gc.collect()
gc.collect() # Run twice for weak references
print_status("[Restart] Garbage collection complete", "info")
print_status("[Restart] Cleanup complete. Ready for re-initialization.", "info")
return True

View File

@@ -6,12 +6,10 @@ HTTP客户端模块
import json
import os
import time
from threading import Thread
from typing import List, Dict, Any, Optional
import requests
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.resources.resource_tracker import ResourceTreeSet
from unilabos.utils.log import info
from unilabos.config.config import HTTPConfig, BasicConfig
from unilabos.utils import logger

View File

@@ -6,7 +6,6 @@ Web服务器模块
import webbrowser
import uvicorn
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import Response
@@ -96,7 +95,7 @@ def setup_server() -> FastAPI:
return app
def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = True) -> None:
def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = True) -> bool:
"""
启动服务器
@@ -104,7 +103,14 @@ def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = T
host: 服务器主机
port: 服务器端口
open_browser: 是否自动打开浏览器
Returns:
bool: True if restart was requested, False otherwise
"""
import threading
import time
from uvicorn import Config, Server
# 设置服务器
setup_server()
@@ -123,7 +129,37 @@ def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = T
# 启动服务器
info(f"[Web] 启动FastAPI服务器: {host}:{port}")
uvicorn.run(app, host=host, port=port, log_config=log_config)
# 使用支持重启的模式
config = Config(app=app, host=host, port=port, log_config=log_config)
server = Server(config)
# 启动服务器线程
server_thread = threading.Thread(target=server.run, daemon=True, name="uvicorn_server")
server_thread.start()
info("[Web] Server started, monitoring for restart requests...")
# 监控重启标志
import unilabos.app.main as main_module
while server_thread.is_alive():
if hasattr(main_module, "_restart_requested") and main_module._restart_requested:
info(
f"[Web] Restart requested via WebSocket, reason: {getattr(main_module, '_restart_reason', 'unknown')}"
)
main_module._restart_requested = False
# 停止服务器
server.should_exit = True
server_thread.join(timeout=5)
info("[Web] Server stopped, ready for restart")
return True
time.sleep(1)
return False
# 当脚本直接运行时启动服务器

View File

@@ -359,7 +359,7 @@ class MessageProcessor:
self.device_manager = device_manager
self.queue_processor = None # 延迟设置
self.websocket_client = None # 延迟设置
self.session_id = ""
self.session_id = str(uuid.uuid4())[:6] # 产生一个随机的session_id
# WebSocket连接
self.websocket = None
@@ -488,7 +488,16 @@ class MessageProcessor:
async for message in self.websocket:
try:
data = json.loads(message)
await self._process_message(data)
message_type = data.get("action", "")
message_data = data.get("data")
if self.session_id and self.session_id == data.get("edge_session"):
await self._process_message(message_type, message_data)
else:
if message_type.endswith("_material"):
logger.trace(f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}")
logger.debug(f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}")
else:
await self._process_message(message_type, message_data)
except json.JSONDecodeError:
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
except Exception as e:
@@ -554,11 +563,8 @@ class MessageProcessor:
finally:
logger.debug("[MessageProcessor] Send handler stopped")
async def _process_message(self, data: Dict[str, Any]):
async def _process_message(self, message_type: str, message_data: Dict[str, Any]):
"""处理收到的消息"""
message_type = data.get("action", "")
message_data = data.get("data")
logger.debug(f"[MessageProcessor] Processing message: {message_type}")
try:
@@ -571,14 +577,19 @@ class MessageProcessor:
elif message_type == "cancel_action" or message_type == "cancel_task":
await self._handle_cancel_action(message_data)
elif message_type == "add_material":
# noinspection PyTypeChecker
await self._handle_resource_tree_update(message_data, "add")
elif message_type == "update_material":
# noinspection PyTypeChecker
await self._handle_resource_tree_update(message_data, "update")
elif message_type == "remove_material":
# noinspection PyTypeChecker
await self._handle_resource_tree_update(message_data, "remove")
elif message_type == "session_id":
self.session_id = message_data.get("session_id")
logger.info(f"[MessageProcessor] Session ID: {self.session_id}")
# elif message_type == "session_id":
# self.session_id = message_data.get("session_id")
# logger.info(f"[MessageProcessor] Session ID: {self.session_id}")
elif message_type == "request_restart":
await self._handle_request_restart(message_data)
else:
logger.debug(f"[MessageProcessor] Unknown message type: {message_type}")
@@ -837,7 +848,7 @@ class MessageProcessor:
device_action_groups[key_add].append(item["uuid"])
logger.info(
f"[MessageProcessor] Resource migrated: {item['uuid'][:8]} from {device_old_id} to {device_id}"
f"[资源同步] 跨站Transfer: {item['uuid'][:8]} from {device_old_id} to {device_id}"
)
else:
# 正常update
@@ -852,11 +863,11 @@ class MessageProcessor:
device_action_groups[key] = []
device_action_groups[key].append(item["uuid"])
logger.info(f"触发物料更新 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}")
logger.trace(f"[资源同步] 动作 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}")
# 为每个(device_id, action)创建独立的更新线程
for (device_id, actual_action), items in device_action_groups.items():
logger.info(f"设备 {device_id} 物料更新 {actual_action} 数量: {len(items)}")
logger.trace(f"[资源同步] {device_id} 物料动作 {actual_action} 数量: {len(items)}")
def _notify_resource_tree(dev_id, act, item_list):
try:
@@ -888,6 +899,49 @@ class MessageProcessor:
)
thread.start()
async def _handle_request_restart(self, data: Dict[str, Any]):
"""
处理重启请求
当LabGo发送request_restart时执行清理并触发重启
"""
reason = data.get("reason", "unknown")
delay = data.get("delay", 2) # 默认延迟2秒
logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s")
# 发送确认消息
if self.websocket_client:
await self.websocket_client.send_message({
"action": "restart_acknowledged",
"data": {"reason": reason, "delay": delay}
})
# 设置全局重启标志
import unilabos.app.main as main_module
main_module._restart_requested = True
main_module._restart_reason = reason
# 延迟后执行清理
await asyncio.sleep(delay)
# 在新线程中执行清理,避免阻塞当前事件循环
def do_cleanup():
import time
time.sleep(0.5) # 给当前消息处理完成的时间
logger.info(f"[MessageProcessor] Starting cleanup for restart, reason: {reason}")
try:
from unilabos.app.utils import cleanup_for_restart
if cleanup_for_restart():
logger.info("[MessageProcessor] Cleanup successful, main() will restart")
else:
logger.error("[MessageProcessor] Cleanup failed")
except Exception as e:
logger.error(f"[MessageProcessor] Error during cleanup: {e}")
cleanup_thread = threading.Thread(target=do_cleanup, name="RestartCleanupThread", daemon=True)
cleanup_thread.start()
logger.info(f"[MessageProcessor] Restart cleanup scheduled")
async def _send_action_state_response(
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
):
@@ -1240,7 +1294,7 @@ class WebSocketClient(BaseCommunicationClient):
},
}
self.message_processor.send_message(message)
logger.debug(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
def publish_job_status(
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None
@@ -1282,7 +1336,7 @@ class WebSocketClient(BaseCommunicationClient):
self.message_processor.send_message(message)
job_log = format_job_log(item.job_id, item.task_id, item.device_id, item.action_name)
logger.debug(f"[WebSocketClient] Job status published: {job_log} - {status}")
logger.trace(f"[WebSocketClient] Job status published: {job_log} - {status}")
def send_ping(self, ping_id: str, timestamp: float) -> None:
"""发送ping消息"""
@@ -1313,17 +1367,55 @@ class WebSocketClient(BaseCommunicationClient):
logger.warning(f"[WebSocketClient] Failed to cancel job {job_log}")
def publish_host_ready(self) -> None:
"""发布host_node ready信号"""
"""发布host_node ready信号,包含设备和动作信息"""
if self.is_disabled or not self.is_connected():
logger.debug("[WebSocketClient] Not connected, cannot publish host ready signal")
return
# 收集设备信息
devices = []
machine_name = BasicConfig.machine_name
try:
host_node = HostNode.get_instance(0)
if host_node:
# 获取设备信息
for device_id, namespace in host_node.devices_names.items():
device_key = f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}"
is_online = device_key in host_node._online_devices
# 获取设备的动作信息
actions = {}
for action_id, client in host_node._action_clients.items():
# action_id 格式: /namespace/device_id/action_name
if device_id in action_id:
action_name = action_id.split("/")[-1]
actions[action_name] = {
"action_path": action_id,
"action_type": str(type(client).__name__),
}
devices.append({
"device_id": device_id,
"namespace": namespace,
"device_key": device_key,
"is_online": is_online,
"machine_name": host_node.device_machine_names.get(device_id, machine_name),
"actions": actions,
})
logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready")
except Exception as e:
logger.warning(f"[WebSocketClient] Error collecting device info: {e}")
message = {
"action": "host_node_ready",
"data": {
"status": "ready",
"timestamp": time.time(),
"machine_name": machine_name,
"devices": devices,
},
}
self.message_processor.send_message(message)
logger.info("[WebSocketClient] Host node ready signal published")
logger.info(f"[WebSocketClient] Host node ready signal published with {len(devices)} devices")

View File

@@ -16,6 +16,7 @@ class BasicConfig:
upload_registry = False
machine_name = "undefined"
vis_2d_enable = False
no_update_feedback = False
enable_resource_load = True
communication_protocol = "websocket"
startup_json_path = None # 填写绝对路径

View File

@@ -6,7 +6,7 @@ Coin Cell Assembly Workstation
"""
from typing import Dict, Any, List, Optional, Union
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
from unilabos.resources.resource_tracker import DeviceNodeResourceTracker
from unilabos.device_comms.workstation_base import WorkstationBase, WorkflowInfo
from unilabos.device_comms.workstation_communication import (
WorkstationCommunicationBase, CommunicationConfig, CommunicationProtocol, CoinCellCommunication
@@ -61,7 +61,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# 创建资源跟踪器(如果没有提供)
if resource_tracker is None:
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
from unilabos.resources.resource_tracker import DeviceNodeResourceTracker
resource_tracker = DeviceNodeResourceTracker()
# 初始化基类

File diff suppressed because it is too large Load Diff

View File

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

73
unilabos/devices/LICENSE Normal file
View File

@@ -0,0 +1,73 @@
Uni-Lab-OS软件许可使用准则
本软件使用准则(以下简称"本准则"旨在规范用户在使用Uni-Lab-OS软件以下简称"本软件")过程中的行为和义务。在下载、安装、使用或以任何方式访问本软件之前,请务必仔细阅读并理解以下条款和条件。若您不同意本准则的全部或部分内容,请您立即停止使用本软件。一旦您开始访问、下载、安装、使用本软件,即表示您已阅读、理解并同意接受本准则的约束。
1、使用许可
1.1 本软件的所有权及版权归北京深势科技有限公司(以下简称"深势科技")所有。在遵守本准则的前提下,深势科技特此授予学术用户(以下简称"您")一个全球范围内的、非排他性的、免版权费用的使用许可,可为了满足学术目的而使用本软件。
1.2 本准则下授予的许可仅适用于本软件的二进制代码版本。您不对本软件源代码拥有任何权利。
2、使用限制
2.1 本准则仅授予学术用户出于学术目的使用本软件,任何商业组织、商业机构或其他非学术用户不得使用本软件,如果违反本条款,深势科技将保留一切追诉的权利。
2.2 您将本软件用于任何商业行为,应取得深势科技的商业许可。
2.3 您不得将本软件或任何形式的衍生作品用于任何商业目的,也不得将其出售、出租、转让、分发或以其他方式提供给任何第三方。您必须确保本软件的使用仅限于您个人学术研究,禁止您为任何其他实体的利益使用本软件(无论是否收费)。
2.4 您不得以任何方式修改、破解、反编译、反汇编、反向工程、隔离、分离或以其他方式从任何程序或文档中提取源代码或试图发现本软件的源代码。您不得以任何方式去除、修改或屏蔽本软件中的任何版权、商标或其他专有权利声明。您不得使用本软件进行任何非法活动,包括但不限于侵犯他人的知识产权、隐私权等。
2.5 您同意将本软件仅用于合法的学术目的,且遵守您所在国家或地区的法律法规,您将承担因违反法律法规而产生的一切法律责任。
3、软件所有权
本软件在此仅作使用许可,并非出售。本软件及与软件有关的全部文档的所有权及其他所有权利(包括但不限于知识产权和商业秘密),始终是深势科技的专有财产,您不拥有任何权利,但本准则下被明确授予的有限的使用许可权利除外。
4、衍生作品传播规范
若您传播基于Uni-Lab-OS程序修改形成的作品须同时满足以下全部条件
4.1 作品必须包含显著声明,明确标注修改内容及修改日期;
4.2 作品必须声明本作品依据本许可协议发布;
4.3 必须将整个作品(包括修改部分)作为整体授予获取副本者本许可协议的保障,且该许可将自动延伸适用于作品全组件(无论其以何种形式打包);
4.4 若衍生作品含交互式用户界面每个界面均须显示合规法律声明若原始Uni-Lab-OS程序的交互界面未展示法律声明您的衍生作品可免除此义务。
5、提出建议
您可以对本软件提出建议,前提是:
i您声明并保证该建议未侵害任何第三方的任何知识产权
ii您承认深势科技有权使用该建议但无使用该建议的义务
iii您授予深势科技一项非独占的、不可撤销的、可分许可的、无版权费的、全球范围的著作权许可以复制、分发、传播、公开展示、公开表演、修改、翻译、基于其制作衍生作品、生产、制作、推销、销售、提供销售和/或以其他方式整体或部分地使用该建议和基于其的衍生作品,包括但不限于,通过将该建议整体或部分地纳入深势科技的软件和/或其他软件,以及在现存的或将来任何时候存在的任何媒介中或通过该媒介体现,以及为从事上述活动而授予多个分许可;
iv您特此授予深势科技一项永久的、全球范围的、非独占性的、免费的、免特许权使用费的、不可撤销的专利许可许可其制造、委托制造、使用、要约销售、销售、进口及以其他方式转让该建议和基于其的衍生专利。上述专利许可的适用范围仅限于以下专利权利要求您有权许可的、且仅因您的建议本身或因您的建议与所提交的本软件结合而必然构成侵权的专利权利要求。若任何实体针对您或其他实体提起专利诉讼包括诉讼中的交叉诉讼或反诉主张该建议或您所贡献的软件构成直接或间接专利侵权则依据本协议授予的、针对该建议或软件的任何专利许可自该诉讼提起之日起终止。
v您放弃对该建议的任何权利或主张深势科技无需承担任何义务、版税或基于知识产权或其他方面的限制。
6、引用要求
如您使用本软件获得的成果发表在出版物上您应在成果中承认对Uni-Lab-OS软件的使用并标注权利人名称。引用 Uni-Lab-OS时请使用以下内容
@article{gao2025unilabos,
title = {UniLabOS: An AI-Native Operating System for Autonomous Laboratories},
doi = {10.48550/arXiv.2512.21766},
publisher = {arXiv},
author = {Gao, Jing and Chang, Junhan and Que, Haohui and Xiong, Yanfei and Zhang, Shixiang and Qi, Xianwei and Liu, Zhen and Wang, Jun-Jie and Ding, Qianjun and Li, Xinyu and Pan, Ziwei and Xie, Qiming and Yan, Zhuang and Yan, Junchi and Zhang, Linfeng},
year = {2025}
}
7、保留权利
您认可,所有未被明确授予您的本软件的权利,无论是当前或今后存在的,均由深势科技予以保留,任何未经深势科技明确授权而使用本软件的行为将被视为侵权,深势科技有权追究侵权者的一切法律责任。
8、保密信息
您同意将本软件代码及相关文档视为深势科技的机密信息,您不会向任何第三方提供相关代码,并将采取合理审慎的使用态度来防止本软件代码及相关文档被泄露。
9、无保证
该软件是"按原样"提供的,没有任何明示或暗示的保证,不包含任何代码或规范没有缺陷、适销性、适用于特定目的或不侵犯第三方权利的保证。您同意您自主承担使用本软件或与本准则有关的全部风险。
10、免责条款
在任何情况下,无论基于侵权(包括过失)、合同或其他法律理论,除非适用法律强制规定(如故意或重大过失行为)或另有书面协议,深势科技不对被许可人因软件许可、使用或无法使用软件所致损害承担责任(包括任何性质的直接、间接、特殊、偶发或后果性损害,例如但不限于商誉损失、停工损失、计算机故障或失灵造成的损害,以及其他一切商业损害或损失),即使深势科技已被告知发生此类损害的可能性亦不例外。
被许可人在再分发软件或其衍生作品时,仅能以自身名义独立承担责任进行操作,不得代表深势科技或其他被许可人。
11、终止
如果您以任何方式违反本准则或未能遵守本准则的任何重要条款或条件,则您被授予的所有权利将自动终止。
12、举报
如果您认为有人违反了本准则请向深势科技进行举报深势科技将对您的身份进行严格保密举报邮箱changjh@dp.tech。
13、法律管辖
本准则中的任何内容均不得解释为通过暗示、禁止反悔或其他方式授予本准则中授予的许可或权利以外的任何许可或权利。如果本准则的任何条款被认定为不可执行,则仅在必要的范围内对该条款进行修改,使其可执行。本准则应受中华人民共和国法律管辖,不适用法律冲突条款及《联合国国际货物销售合同公约》,因本准则产生的一切争议由北京市海淀区人民法院管辖。
14、未来版本
深势科技保留不经事先通知随时变更或停止本软件或本准则的权利。
15、语言优先
本准则同时具有中文版本和英文版本,如果英文版本和中文版本有冲突,以中文版本为准。

View File

@@ -0,0 +1,73 @@
Uni-Lab-OS License Agreement
Preamble
This License Agreement (the "Agreement") is instituted to govern user conduct and obligations in relation to the utilization of the Uni-Lab-OS (the "Software"). By accessing, downloading, installing, or utilizing the Software in any manner, you hereby acknowledge that you have meticulously reviewed, comprehended, and consented to be legally bound by the terms herein. If you dissent from any provision of this Agreement, you must forthwith cease all interaction with the Software.
1. Grant of License
1.1 The proprietary rights to the Software are exclusively retained by Beijing DP Technology Co., Ltd. ("DP Technology"). Subject to full compliance with this Agreement, DP Technology hereby grants academic users ("Licensee") a worldwide, non-exclusive, royalty-free license to untilise the Software solely for non-commercial academic pursuits.
1.2 The foregoing license applies exclusively to the Software's executable binary code. No rights whatsoever are conferred to the Software's source code.
2. Usage Restrictions
2.1 This license is restricted to academic users engaging in scholastic activities. Commercial entities, institutions, or any non-academic parties are expressly prohibited from utilizing the Software. Violations of this clause shall entitle DP Technology to pursue all available legal remedies.
2.2 The Licensee shall obtain a commercial license from DP Technology for any commercial use of the Software.
2.3 The Licensee shall not utilise the Software or any derivative works for commercial purposes, nor distribute, sublicense, lease, transfer, or otherwise disseminate the Software to third parties. The Licensee is strictly prohibited from utilizing the Software for the benefit of any third-party entity, whether gratuitously or otherwise.
2.4 Reverse engineering, decompilation, disassembly, code isolation, or any attempt to derive source code from the Software is strictly prohibited. The Licensee shall not alter, circumvent, or remove copyright notices, trademarks, or proprietary legends embedded in the Software. Use of the Software for unlawful activities—including but not limited to intellectual property infringement or privacy violations—is categorically barred.
2.5 The Licensee warrants that the Software shall be utilised solely for lawful academic purposes in compliance with applicable jurisdictional statutes. All legal liabilities arising from noncompliance shall be borne exclusively by the Licensee.
3. Proprietary Rights
This Agreement confers a license to utilise the Software, not a transfer of ownership. All intellectual property rights—including copyrights, patents, trade secrets, and documentation—remain the exclusive dominion of DP Technology. The Licensee acquires no entitlements beyond the limited usage privileges expressly delineated herein.
4. Derivative Work
You may convey a work based on the Software, or the modifications to produce it from the Software, provided that you meet all of these conditions:
4.1 The work must carry prominent notices stating that you modified it, and giving a relevant date.
4.2 The work must carry prominent notices stating that it is released under this License.
4.3 You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
4.4 If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Software has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
5. Feedback and Proposals
Licensees may submit proposals, suggestions, or improvements pertaining to the Software ("Feedback") under the following conditions:
(a) Licensee represents and warrants that such Feedback does not infringe upon any third-party intellectual property rights;
(b) Licensee acknowledges that DP Technology reserves the right, but assumes no obligation, to utilize such Feedback;
(c) Licensee irrevocably grants DP Technology a non-exclusive, royalty-free, perpetual, worldwide, sublicensable copyright license to reproduce, distribute, modify, publicly perform or display, translate, create derivative works of, commercialize, and otherwise exploit the Feedback in any medium or format, whether now known or hereafter devised, including the right to grant multiple tiers of sublicenses to enable such activities;
(d) Licensee hereby grants DP Technology a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Feedback and such Derivative Works, where such license applies only to those patent claimss licensable by Licensee that are necessarily infringed by the Feedback(s) alone or by comibination of the Feedback(s) with the Software to which such Feedback(s) were submitted. If any entity institutes patent litigation against Licensee or any other entity (including a cross-claim orcounterclaim in a lawsuit) alleging that the Feedback, or the Software to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted under this Agreement for the Feedback or Software shall terminate as of the date such litigation is filed.
(e) Licensee hereby waives all claims, proprietary rights, or restrictions related to DP Technology's use of such Feedback.
6. Citation Requirement
If academic or research output generated using the Software is published, Licensee must explicitly acknowledge the use of Uni-Lab-OS and attribute ownership to DP Technology. The following citation must be included:
@article{gao2025unilabos,
title = {UniLabOS: An AI-Native Operating System for Autonomous Laboratories},
doi = {10.48550/arXiv.2512.21766},
publisher = {arXiv},
author = {Gao, Jing and Chang, Junhan and Que, Haohui and Xiong, Yanfei and Zhang, Shixiang and Qi, Xianwei and Liu, Zhen and Wang, Jun-Jie and Ding, Qianjun and Li, Xinyu and Pan, Ziwei and Xie, Qiming and Yan, Zhuang and Yan, Junchi and Zhang, Linfeng},
year = {2025}
}
7. Reservation of Rights
All rights not expressly granted herein, whether existing now or arising in the future, are exclusively reserved by DP Technology. Any unauthorized use of the Software beyond the scope of this Agreement constitutes infringement, and DP Technology reserves all legal rights to pursue remedies against violators.
8. Confidentiality
Licensee agrees to treat the Software's code, documentation, and related materials as confidential information. Licensee shall not disclose such materials to third parties and shall employ reasonable safeguards to prevent unauthorized access, dissemination, or misuse.
9. Disclaimer of Warranties
The software is provided "as is," without warranties of any kind, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, or error-free operation. Licensee accepts all risks associated with the use of the software.
10. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall DP Technology be liable to Licensee for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the software (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if DP Technology has been advised of the possibility of such damages.
While redistributing the Software or Derivative Works thereof, Licensee may act only on Licensee's own behalf and on Licensee's sole responsibility, not on behalf of DP Technology or any other Licensee.
11. Termination
All rights granted herein shall terminate immediately and automatically if Licensee materially breaches any provision of this Agreement.
12. Reporting Violations
To report suspected violations of this Agreement, notify DP Technology via the designated email address: changjh@dp.tech. DP Technology shall maintain the confidentiality of the reporter's identity.
13. Governing Law and Dispute Resolution
This Agreement shall be governed by the laws of the People's Republic of China, excluding its conflict of laws principles and the United Nations Convention on Contracts for the International Sale of Goods. Any dispute arising from this Agreement shall be exclusively adjudicated by the Haidian District People's Court in Beijing.
14. Amendments and Updates
DP Technology reserves the right to modify, suspend, or terminate the Software or this Agreement at any time without prior notice.
15. Language Priority
This Agreement is provided in both Chinese and English. In the event of any discrepancy, the Chinese version shall prevail.

View File

@@ -13,7 +13,7 @@ from pylabrobot.resources import (
import copy
from unilabos_msgs.msg import Resource
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker # type: ignore
from unilabos.resources.resource_tracker import DeviceNodeResourceTracker # type: ignore
class LiquidHandlerBiomek:

View File

@@ -1042,11 +1042,19 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
One or more TipRacks providing fresh tips.
is_96_well
Set *True* to use the 96channel head.
mix_stage
When to mix the target wells relative to dispensing. Default "none" means
no mixing occurs even if mix_times is provided. Use "before", "after", or
"both" to mix at the corresponding stage(s).
mix_times
Number of mix cycles. If *None* (default) no mixing occurs regardless of
mix_stage.
"""
# 确保 use_channels 有默认值
if use_channels is None:
use_channels = [0] if self.channel_num >= 1 else list(range(self.channel_num))
# 默认使用设备所有通道(例如 8 通道移液站默认就是 0-7
use_channels = list(range(self.channel_num)) if self.channel_num > 0 else [0]
if is_96_well:
pass # This mode is not verified.
@@ -1074,42 +1082,42 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
if mix_times is not None:
mix_times = int(mix_times)
# 识别传输模式
num_sources = len(sources)
num_targets = len(targets)
if num_sources == 1 and num_targets > 1:
# 模式1: 一对多 (1 source -> N targets)
await self._transfer_one_to_many(
sources[0], targets, tip_racks, use_channels,
asp_vols, dis_vols, asp_flow_rates, dis_flow_rates,
offsets, touch_tip, liquid_height, blow_out_air_volume,
spread, mix_stage, mix_times, mix_vol, mix_rate,
mix_liquid_height, delays
)
elif num_sources > 1 and num_targets == 1:
# 模式2: 多对一 (N sources -> 1 target)
await self._transfer_many_to_one(
sources, targets[0], tip_racks, use_channels,
asp_vols, dis_vols, asp_flow_rates, dis_flow_rates,
offsets, touch_tip, liquid_height, blow_out_air_volume,
spread, mix_stage, mix_times, mix_vol, mix_rate,
mix_liquid_height, delays
)
elif num_sources == num_targets:
# 模式3: 一对一 (N sources -> N targets) - 原有逻辑
await self._transfer_one_to_one(
sources, targets, tip_racks, use_channels,
asp_vols, dis_vols, asp_flow_rates, dis_flow_rates,
offsets, touch_tip, liquid_height, blow_out_air_volume,
spread, mix_stage, mix_times, mix_vol, mix_rate,
mix_liquid_height, delays
)
else:
raise ValueError(
f"Unsupported transfer mode: {num_sources} sources -> {num_targets} targets. "
"Supported modes: 1->N, N->1, or N->N."
)
# 识别传输模式mix_times 为 None 也应该能正常移液,只是不做 mix
num_sources = len(sources)
num_targets = len(targets)
if num_sources == 1 and num_targets > 1:
# 模式1: 一对多 (1 source -> N targets)
await self._transfer_one_to_many(
sources[0], targets, tip_racks, use_channels,
asp_vols, dis_vols, asp_flow_rates, dis_flow_rates,
offsets, touch_tip, liquid_height, blow_out_air_volume,
spread, mix_stage, mix_times, mix_vol, mix_rate,
mix_liquid_height, delays
)
elif num_sources > 1 and num_targets == 1:
# 模式2: 多对一 (N sources -> 1 target)
await self._transfer_many_to_one(
sources, targets[0], tip_racks, use_channels,
asp_vols, dis_vols, asp_flow_rates, dis_flow_rates,
offsets, touch_tip, liquid_height, blow_out_air_volume,
spread, mix_stage, mix_times, mix_vol, mix_rate,
mix_liquid_height, delays
)
elif num_sources == num_targets:
# 模式3: 一对一 (N sources -> N targets)
await self._transfer_one_to_one(
sources, targets, tip_racks, use_channels,
asp_vols, dis_vols, asp_flow_rates, dis_flow_rates,
offsets, touch_tip, liquid_height, blow_out_air_volume,
spread, mix_stage, mix_times, mix_vol, mix_rate,
mix_liquid_height, delays
)
else:
raise ValueError(
f"Unsupported transfer mode: {num_sources} sources -> {num_targets} targets. "
"Supported modes: 1->N, N->1, or N->N."
)
async def _transfer_one_to_one(
self,
@@ -1149,6 +1157,16 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
tip.extend(next(self.current_tip))
await self.pick_up_tips(tip)
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
await self.mix(
targets=[targets[_]],
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
await self.aspirate(
resources=[sources[_]],
vols=[asp_vols[_]],
@@ -1209,6 +1227,16 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
current_dis_blow_out_air_volume = blow_out_air_volume[i:i + 8] if blow_out_air_volume else [None] * 8
current_dis_flow_rates = dis_flow_rates[i:i + 8] if dis_flow_rates else None
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
await self.mix(
targets=current_targets,
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
await self.aspirate(
resources=current_reagent_sources,
vols=current_asp_vols,
@@ -1290,6 +1318,17 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
tip.extend(next(self.current_tip))
await self.pick_up_tips(tip)
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
for idx, target in enumerate(targets):
await self.mix(
targets=[target],
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets[idx:idx + 1] if offsets and len(offsets) > idx else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
# 从源容器吸液(总体积)
await self.aspirate(
resources=[source],
@@ -1354,6 +1393,16 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
current_asp_liquid_height = liquid_height[0:1] * 8 if liquid_height and len(liquid_height) > 0 else [None] * 8
current_asp_blow_out_air_volume = blow_out_air_volume[0:1] * 8 if blow_out_air_volume and len(blow_out_air_volume) > 0 else [None] * 8
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
await self.mix(
targets=current_targets,
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets[i:i + 8] if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
# 从源容器吸液8个通道都从同一个源但每个通道的吸液体积不同
await self.aspirate(
resources=[source] * 8, # 8个通道都从同一个源
@@ -1452,8 +1501,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
# 单通道模式:多次吸液,一次分液
# 先混合前(如果需要)
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
# 注意:在吸液前混合源容器通常不常见,这里跳过
pass
await self.mix(
targets=[target],
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets[0:1] if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
# 从每个源容器吸液并分液到目标容器
for idx, source in enumerate(sources):
@@ -1528,6 +1583,16 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
raise ValueError(f"For 8-channel mode, number of sources {len(sources)} must be a multiple of 8.")
# 每次处理8个源
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
await self.mix(
targets=[target],
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets[0:1] if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
for i in range(0, len(sources), 8):
tip = []
for _ in range(len(use_channels)):

View File

@@ -70,7 +70,16 @@ class PRCXI9300Deck(Deck):
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, **kwargs):
super().__init__(name, size_x, size_y, size_z)
self.slots = [None] * 6 # PRCXI 9300 有 6 个槽位
self.slots = [None] * 16 # PRCXI 9300/9320 最大16 个槽位
self.slot_locations = [Coordinate(0, 0, 0)] * 16
def assign_child_at_slot(self, resource: Resource, slot: int, reassign: bool = False) -> None:
if self.slots[slot - 1] is not None and not reassign:
raise ValueError(f"Spot {slot} is already occupied")
self.slots[slot - 1] = resource
super().assign_child_resource(resource, location=self.slot_locations[slot - 1])
class PRCXI9300Container(Plate):
"""PRCXI 9300 的专用 Container 类,继承自 Plate用于槽位定位和未知模块。

View File

@@ -0,0 +1,13 @@
"""
说明:
这里放一个“入口文件”,方便在 `unilabos/devices/liquid_handling` 目录下直接找到
`transfer_liquid` 的测试。
实际测试用例实现放在仓库标准测试目录:
`tests/devices/liquid_handling/test_transfer_liquid.py`
"""
# 让 pytest 能从这里发现同一套测试(避免复制两份测试代码)。
from tests.devices.liquid_handling.test_transfer_liquid import * # noqa: F401,F403

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -30,71 +30,6 @@ bioyond_dispensing_station:
title: brief_step_parameters参数
type: object
type: UniLabJsonCommand
auto-compute_experiment_design:
feedback: {}
goal: {}
goal_default:
m_tot: '70'
ratio: null
titration_percent: '0.03'
wt_percent: '0.25'
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
m_tot:
default: '70'
type: string
ratio:
type: object
titration_percent:
default: '0.03'
type: string
wt_percent:
default: '0.25'
type: string
required:
- ratio
type: object
result:
properties:
feeding_order:
items: {}
title: Feeding Order
type: array
return_info:
title: Return Info
type: string
solutions:
items: {}
title: Solutions
type: array
solvents:
additionalProperties: true
title: Solvents
type: object
titration:
additionalProperties: true
title: Titration
type: object
required:
- solutions
- titration
- solvents
- feeding_order
- return_info
title: ComputeExperimentDesignReturn
type: object
required:
- goal
title: compute_experiment_design参数
type: object
type: UniLabJsonCommand
auto-process_order_finish_report:
feedback: {}
goal: {}
@@ -365,6 +300,99 @@ bioyond_dispensing_station:
title: BatchCreateDiamineSolutionTasks
type: object
type: UniLabJsonCommand
compute_experiment_design:
feedback: {}
goal:
m_tot: m_tot
ratio: ratio
titration_percent: titration_percent
wt_percent: wt_percent
goal_default:
m_tot: '70'
ratio: ''
titration_percent: '0.03'
wt_percent: '0.25'
handles:
output:
- data_key: solutions
data_source: executor
data_type: array
handler_key: solutions
io_type: sink
label: Solution Data From Python
- data_key: titration
data_source: executor
data_type: object
handler_key: titration
io_type: sink
label: Titration Data From Calculation Node
- data_key: solvents
data_source: executor
data_type: object
handler_key: solvents
io_type: sink
label: Solvents Data From Calculation Node
- data_key: feeding_order
data_source: executor
data_type: array
handler_key: feeding_order
io_type: sink
label: Feeding Order Data From Calculation Node
result:
feeding_order: feeding_order
return_info: return_info
solutions: solutions
solvents: solvents
titration: titration
schema:
description: 计算实验设计输出solutions/titration/solvents/feeding_order用于后续节点。
properties:
feedback: {}
goal:
properties:
m_tot:
default: '70'
description: 总质量(g)
type: string
ratio:
description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1}
type: string
titration_percent:
default: '0.03'
description: 滴定比例(10%部分)
type: string
wt_percent:
default: '0.25'
description: 目标固含质量分数
type: string
required:
- ratio
type: object
result:
properties:
feeding_order:
type: array
return_info:
type: string
solutions:
type: array
solvents:
type: object
titration:
type: object
required:
- solutions
- titration
- solvents
- feeding_order
- return_info
title: ComputeExperimentDesign_Result
type: object
required:
- goal
title: ComputeExperimentDesign
type: object
type: UniLabJsonCommand
create_90_10_vial_feeding_task:
feedback: {}
goal:
@@ -591,6 +619,89 @@ bioyond_dispensing_station:
title: DispenStationSolnPrep
type: object
type: DispenStationSolnPrep
scheduler_start:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result:
return_info: return_info
schema:
description: 启动调度器 - 启动Bioyond配液站的任务调度器开始执行队列中的任务
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result:
properties:
return_info:
description: 调度器启动结果成功返回1失败返回0
type: integer
required:
- return_info
title: scheduler_start结果
type: object
required:
- goal
title: scheduler_start参数
type: object
type: UniLabJsonCommand
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: 目标堆栈名称(从列表选择)
enum:
- 堆栈1左
- 堆栈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:
@@ -659,7 +770,7 @@ bioyond_dispensing_station:
title: WaitForMultipleOrdersAndGetReports
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation
status_types: {}
type: python
config_info: []
@@ -670,15 +781,16 @@ bioyond_dispensing_station:
config:
properties:
config:
type: string
type: object
deck:
type: string
required:
- config
- deck
protocol_type:
type: string
required: []
type: object
data:
properties: {}
required: []
type: object
model: {}
version: 1.0.0

View File

@@ -9,6 +9,7 @@ cameracontroller_device:
goal_default:
config: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
@@ -31,6 +32,7 @@ cameracontroller_device:
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''

View File

@@ -4,6 +4,73 @@ separator.chinwe:
- chinwe
class:
action_value_mappings:
auto-connect:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: connect参数
type: object
type: UniLabJsonCommand
auto-disconnect:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: disconnect参数
type: object
type: UniLabJsonCommand
auto-execute_command_from_outer:
feedback: {}
goal: {}
goal_default:
command_dict: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
command_dict:
type: object
required:
- command_dict
type: object
result: {}
required:
- goal
title: execute_command_from_outer参数
type: object
type: UniLabJsonCommand
motor_rotate_quarter:
goal:
direction: 顺时针
@@ -303,42 +370,44 @@ separator.chinwe:
handles: []
icon: ''
init_param_schema:
goal:
baudrate:
default: 9600
description: 串口波特率
type: integer
motor_ids:
default:
- 4
- 5
description: 步进电机ID列表
items:
config:
properties:
baudrate:
default: 9600
type: integer
type: array
port:
default: 192.168.1.200:8899
description: 串口号或 IP:Port
type: string
pump_ids:
default:
- 1
- 2
- 3
description: 注射泵ID列表
items:
motor_ids:
items:
type: integer
type: array
port:
default: 192.168.1.200:8899
type: string
pump_ids:
items:
type: integer
type: array
sensor_id:
default: 6
type: integer
type: array
sensor_id:
default: 6
description: XKC传感器ID
type: integer
sensor_threshold:
default: 300
description: 传感器液位判定阈值
type: integer
timeout:
default: 10
description: 通信超时时间 (秒)
type: integer
sensor_threshold:
default: 300
type: integer
timeout:
default: 10.0
type: number
required: []
type: object
data:
properties:
is_connected:
type: boolean
sensor_level:
type: boolean
sensor_rssi:
type: integer
required:
- sensor_level
- sensor_rssi
- is_connected
type: object
version: 2.1.0

View File

@@ -0,0 +1,850 @@
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_allpack_cmd_simp:
feedback: {}
goal: {}
goal_default:
assembly_pressure: 4200
assembly_type: 7
battery_clean_ignore: false
battery_pressure_mode: true
dual_drop_first_volume: 25
dual_drop_mode: false
dual_drop_start_timing: false
dual_drop_suction_timing: false
elec_num: null
elec_use_num: null
elec_vol: 50
file_path: /Users/sml/work
fujipian_juzhendianwei: 0
fujipian_panshu: 0
gemo_juzhendianwei: 0
gemopanshu: 0
lvbodian: true
qiangtou_juzhendianwei: 0
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 简化版电池组装函数,整合了参数设置和双滴模式
properties:
feedback: {}
goal:
properties:
assembly_pressure:
default: 4200
description: 电池压制力(N)
type: integer
assembly_type:
default: 7
description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫)
type: integer
battery_clean_ignore:
default: false
description: 是否忽略电池清洁步骤
type: boolean
battery_pressure_mode:
default: true
description: 是否启用压力模式
type: boolean
dual_drop_first_volume:
default: 25
description: 二次滴液第一次排液体积(μL)
type: integer
dual_drop_mode:
default: false
description: 电解液添加模式(false=单次滴液, true=二次滴液)
type: boolean
dual_drop_start_timing:
default: false
description: 二次滴液开始滴液时机(false=正极片前, true=正极片后)
type: boolean
dual_drop_suction_timing:
default: false
description: 二次滴液吸液时机(false=正常吸液, true=先吸液)
type: boolean
elec_num:
description: 电解液瓶数
type: string
elec_use_num:
description: 每瓶电解液组装电池数
type: string
elec_vol:
default: 50
description: 电解液吸液量(μL)
type: integer
file_path:
default: /Users/sml/work
description: 实验记录保存路径
type: string
fujipian_juzhendianwei:
default: 0
description: 负极片矩阵点位。盘位置从1开始计数有效范围1-8, 13-20 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
type: integer
fujipian_panshu:
default: 0
description: 负极片盘数
type: integer
gemo_juzhendianwei:
default: 0
description: 隔膜矩阵点位。盘位置从1开始计数有效范围1-8, 13-20 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
type: integer
gemopanshu:
default: 0
description: 隔膜盘数
type: integer
lvbodian:
default: true
description: 是否使用铝箔垫片
type: boolean
qiangtou_juzhendianwei:
default: 0
description: 枪头盒矩阵点位。盘位置从1开始计数有效范围1-32, 64-96 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
type: integer
required:
- elec_num
- elec_use_num
type: object
result: {}
required:
- goal
title: func_allpack_cmd_simp参数
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_init_auto_start_combined:
feedback: {}
goal: {}
goal_default:
material_search_enable: false
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 组合函数:设备初始化 + 物料搜寻确认 + 切换自动模式 + 启动。初始化过程中会自动检测物料搜寻确认弹窗,并根据参数自动点击"是"或"否"按钮
properties:
feedback: {}
goal:
properties:
material_search_enable:
default: false
description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻)
type: boolean
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_init_auto_start_combined参数
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:
input:
- data_key: bottle_num
data_source: workflow
data_type: integer
handler_key: bottle_count
io_type: source
label: 配液瓶数
required: true
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_sendbottle_allpack_multi:
feedback: {}
goal: {}
goal_default:
assembly_pressure: 4200
assembly_type: 7
battery_clean_ignore: false
battery_pressure_mode: true
dual_drop_first_volume: 25
dual_drop_mode: false
dual_drop_start_timing: false
dual_drop_suction_timing: false
elec_num: null
elec_use_num: null
elec_vol: 50
file_path: /Users/sml/work
fujipian_juzhendianwei: 0
fujipian_panshu: 0
gemo_juzhendianwei: 0
gemopanshu: 0
lvbodian: true
qiangtou_juzhendianwei: 0
handles:
input:
- data_key: elec_num
data_source: workflow
data_type: integer
handler_key: bottle_count
io_type: source
label: 配液瓶数
required: true
placeholder_keys: {}
result: {}
schema:
description: 发送瓶数+简化组装函数(适用于第二批次及后续批次),合并了发送瓶数和简化组装流程
properties:
feedback: {}
goal:
properties:
assembly_pressure:
default: 4200
description: 电池压制力(N)
type: integer
assembly_type:
default: 7
description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫)
type: integer
battery_clean_ignore:
default: false
description: 是否忽略电池清洁步骤
type: boolean
battery_pressure_mode:
default: true
description: 是否启用压力模式
type: boolean
dual_drop_first_volume:
default: 25
description: 二次滴液第一次排液体积(μL)
type: integer
dual_drop_mode:
default: false
description: 电解液添加模式(false=单次滴液, true=二次滴液)
type: boolean
dual_drop_start_timing:
default: false
description: 二次滴液开始滴液时机(false=正极片前, true=正极片后)
type: boolean
dual_drop_suction_timing:
default: false
description: 二次滴液吸液时机(false=正常吸液, true=先吸液)
type: boolean
elec_num:
description: 电解液瓶数如果在workflow中已通过handles连接上游(create_orders的bottle_count输出),则此参数会自动从上游获取,无需手动填写;如果单独使用此函数(没有上游连接),则必须手动填写电解液瓶数
type: string
elec_use_num:
description: 每瓶电解液组装电池数
type: string
elec_vol:
default: 50
description: 电解液吸液量(μL)
type: integer
file_path:
default: /Users/sml/work
description: 实验记录保存路径
type: string
fujipian_juzhendianwei:
default: 0
description: 负极片矩阵点位。盘位置从1开始计数有效范围1-8, 13-20 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
type: integer
fujipian_panshu:
default: 0
description: 负极片盘数
type: integer
gemo_juzhendianwei:
default: 0
description: 隔膜矩阵点位。盘位置从1开始计数有效范围1-8, 13-20 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
type: integer
gemopanshu:
default: 0
description: 隔膜盘数
type: integer
lvbodian:
default: true
description: 是否使用铝箔垫片
type: boolean
qiangtou_juzhendianwei:
default: 0
description: 枪头盒矩阵点位。盘位置从1开始计数有效范围1-32, 64-96 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
type: integer
required:
- elec_num
- elec_use_num
type: object
result: {}
required:
- goal
title: func_sendbottle_allpack_multi参数
type: object
type: UniLabJsonCommand
auto-func_stop_read_data:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_stop_read_data参数
type: object
type: UniLabJsonCommand
auto-modify_deck_name:
feedback: {}
goal: {}
goal_default:
resource_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
resource_name:
type: string
required:
- resource_name
type: object
result: {}
required:
- goal
title: modify_deck_name参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-qiming_coin_cell_code:
feedback: {}
goal: {}
goal_default:
battery_clean_ignore: false
battery_pressure: 4000
battery_pressure_mode: true
fujipian_juzhendianwei: 0
fujipian_panshu: null
gemo_juzhendianwei: 0
gemopanshu: 0
lvbodian: true
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
battery_clean_ignore:
default: false
type: boolean
battery_pressure:
default: 4000
type: integer
battery_pressure_mode:
default: true
type: boolean
fujipian_juzhendianwei:
default: 0
type: integer
fujipian_panshu:
type: integer
gemo_juzhendianwei:
default: 0
type: integer
gemopanshu:
default: 0
type: integer
lvbodian:
default: true
type: boolean
required:
- fujipian_panshu
type: object
result: {}
required:
- goal
title: qiming_coin_cell_code参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
status_types:
data_assembly_coin_cell_num: int
data_assembly_pressure: int
data_assembly_time: float
data_axis_x_pos: float
data_axis_y_pos: float
data_axis_z_pos: float
data_coin_cell_code: str
data_coin_num: int
data_electrolyte_code: str
data_electrolyte_volume: int
data_glove_box_o2_content: float
data_glove_box_pressure: float
data_glove_box_water_content: float
data_open_circuit_voltage: float
data_pole_weight: float
request_rec_msg_status: bool
request_send_msg_status: bool
sys_mode: str
sys_status: str
type: python
config_info: []
description: ''
handles: []
icon: koudian.webp
init_param_schema:
config:
properties:
address:
default: 172.16.28.102
type: string
config:
type: object
debug_mode:
default: false
type: boolean
deck:
type: string
port:
default: '502'
type: string
required: []
type: object
data:
properties:
data_assembly_coin_cell_num:
type: integer
data_assembly_pressure:
type: integer
data_assembly_time:
type: number
data_axis_x_pos:
type: number
data_axis_y_pos:
type: number
data_axis_z_pos:
type: number
data_coin_cell_code:
type: string
data_coin_num:
type: integer
data_electrolyte_code:
type: string
data_electrolyte_volume:
type: integer
data_glove_box_o2_content:
type: number
data_glove_box_pressure:
type: number
data_glove_box_water_content:
type: number
data_open_circuit_voltage:
type: number
data_pole_weight:
type: number
request_rec_msg_status:
type: boolean
request_send_msg_status:
type: boolean
sys_mode:
type: string
sys_status:
type: string
required:
- sys_status
- sys_mode
- request_rec_msg_status
- request_send_msg_status
- data_assembly_coin_cell_num
- data_assembly_time
- data_open_circuit_voltage
- data_axis_x_pos
- data_axis_y_pos
- data_axis_z_pos
- data_pole_weight
- data_assembly_pressure
- data_electrolyte_volume
- data_coin_num
- data_coin_cell_code
- data_electrolyte_code
- data_glove_box_pressure
- data_glove_box_o2_content
- data_glove_box_water_content
type: object
registry_type: device
version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,11 @@ xyz_stepper_controller:
- laiyu_liquid_test
class:
action_value_mappings:
auto-define_current_as_zero:
auto-degrees_to_steps:
feedback: {}
goal: {}
goal_default:
save_path: work_origin.json
degrees: null
handles: {}
placeholder_keys: {}
result: {}
@@ -17,23 +17,22 @@ xyz_stepper_controller:
feedback: {}
goal:
properties:
save_path:
default: work_origin.json
type: string
required: []
degrees:
type: number
required:
- degrees
type: object
result: {}
required:
- goal
title: define_current_as_zero参数
title: degrees_to_steps参数
type: object
type: UniLabJsonCommand
auto-enable:
auto-emergency_stop:
feedback: {}
goal: {}
goal_default:
axis: null
state: null
handles: {}
placeholder_keys: {}
result: {}
@@ -44,27 +43,415 @@ xyz_stepper_controller:
goal:
properties:
axis:
type: string
state:
type: boolean
type: object
required:
- axis
- state
type: object
result: {}
required:
- goal
title: enable参数
title: emergency_stop参数
type: object
type: UniLabJsonCommand
auto-move_to:
auto-enable_all_axes:
feedback: {}
goal: {}
goal_default:
enable: true
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
enable:
default: true
type: boolean
required: []
type: object
result: {}
required:
- goal
title: enable_all_axes参数
type: object
type: UniLabJsonCommand
auto-enable_motor:
feedback: {}
goal: {}
goal_default:
acc: 500
axis: null
precision: 50
speed: 2000
enable: true
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
axis:
type: object
enable:
default: true
type: boolean
required:
- axis
type: object
result: {}
required:
- goal
title: enable_motor参数
type: object
type: UniLabJsonCommand
auto-home_all_axes:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: home_all_axes参数
type: object
type: UniLabJsonCommand
auto-home_axis:
feedback: {}
goal: {}
goal_default:
axis: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
axis:
type: object
required:
- axis
type: object
result: {}
required:
- goal
title: home_axis参数
type: object
type: UniLabJsonCommand
auto-move_to_position:
feedback: {}
goal: {}
goal_default:
acceleration: 1000
axis: null
position: null
precision: 100
speed: 5000
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
acceleration:
default: 1000
type: integer
axis:
type: object
position:
type: integer
precision:
default: 100
type: integer
speed:
default: 5000
type: integer
required:
- axis
- position
type: object
result: {}
required:
- goal
title: move_to_position参数
type: object
type: UniLabJsonCommand
auto-move_to_position_degrees:
feedback: {}
goal: {}
goal_default:
acceleration: 1000
axis: null
degrees: null
precision: 100
speed: 5000
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
acceleration:
default: 1000
type: integer
axis:
type: object
degrees:
type: number
precision:
default: 100
type: integer
speed:
default: 5000
type: integer
required:
- axis
- degrees
type: object
result: {}
required:
- goal
title: move_to_position_degrees参数
type: object
type: UniLabJsonCommand
auto-move_to_position_revolutions:
feedback: {}
goal: {}
goal_default:
acceleration: 1000
axis: null
precision: 100
revolutions: null
speed: 5000
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
acceleration:
default: 1000
type: integer
axis:
type: object
precision:
default: 100
type: integer
revolutions:
type: number
speed:
default: 5000
type: integer
required:
- axis
- revolutions
type: object
result: {}
required:
- goal
title: move_to_position_revolutions参数
type: object
type: UniLabJsonCommand
auto-move_xyz:
feedback: {}
goal: {}
goal_default:
acceleration: 1000
speed: 5000
x: null
y: null
z: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
acceleration:
default: 1000
type: integer
speed:
default: 5000
type: integer
x:
type: string
y:
type: string
z:
type: string
required: []
type: object
result: {}
required:
- goal
title: move_xyz参数
type: object
type: UniLabJsonCommand
auto-move_xyz_degrees:
feedback: {}
goal: {}
goal_default:
acceleration: 1000
speed: 5000
x_deg: null
y_deg: null
z_deg: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
acceleration:
default: 1000
type: integer
speed:
default: 5000
type: integer
x_deg:
type: string
y_deg:
type: string
z_deg:
type: string
required: []
type: object
result: {}
required:
- goal
title: move_xyz_degrees参数
type: object
type: UniLabJsonCommand
auto-move_xyz_revolutions:
feedback: {}
goal: {}
goal_default:
acceleration: 1000
speed: 5000
x_rev: null
y_rev: null
z_rev: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
acceleration:
default: 1000
type: integer
speed:
default: 5000
type: integer
x_rev:
type: string
y_rev:
type: string
z_rev:
type: string
required: []
type: object
result: {}
required:
- goal
title: move_xyz_revolutions参数
type: object
type: UniLabJsonCommand
auto-revolutions_to_steps:
feedback: {}
goal: {}
goal_default:
revolutions: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
revolutions:
type: number
required:
- revolutions
type: object
result: {}
required:
- goal
title: revolutions_to_steps参数
type: object
type: UniLabJsonCommand
auto-set_speed_mode:
feedback: {}
goal: {}
goal_default:
acceleration: 1000
axis: null
speed: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
acceleration:
default: 1000
type: integer
axis:
type: object
speed:
type: integer
required:
- axis
- speed
type: object
result: {}
required:
- goal
title: set_speed_mode参数
type: object
type: UniLabJsonCommand
auto-steps_to_degrees:
feedback: {}
goal: {}
goal_default:
steps: null
handles: {}
placeholder_keys: {}
@@ -75,38 +462,22 @@ xyz_stepper_controller:
feedback: {}
goal:
properties:
acc:
default: 500
type: integer
axis:
type: string
precision:
default: 50
type: integer
speed:
default: 2000
type: integer
steps:
type: integer
required:
- axis
- steps
type: object
result: {}
required:
- goal
title: move_to参数
title: steps_to_degrees参数
type: object
type: UniLabJsonCommand
auto-move_xyz_work:
auto-steps_to_revolutions:
feedback: {}
goal: {}
goal_default:
acc: 1500
speed: 100
x: 0.0
y: 0.0
z: 0.0
steps: null
handles: {}
placeholder_keys: {}
result: {}
@@ -116,35 +487,21 @@ xyz_stepper_controller:
feedback: {}
goal:
properties:
acc:
default: 1500
steps:
type: integer
speed:
default: 100
type: integer
x:
default: 0.0
type: number
y:
default: 0.0
type: number
z:
default: 0.0
type: number
required: []
required:
- steps
type: object
result: {}
required:
- goal
title: move_xyz_work参数
title: steps_to_revolutions参数
type: object
type: UniLabJsonCommand
auto-return_to_work_origin:
auto-stop_all_axes:
feedback: {}
goal: {}
goal_default:
acc: 800
speed: 200
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
@@ -153,22 +510,16 @@ xyz_stepper_controller:
properties:
feedback: {}
goal:
properties:
acc:
default: 800
type: integer
speed:
default: 200
type: integer
properties: {}
required: []
type: object
result: {}
required:
- goal
title: return_to_work_origin参数
title: stop_all_axes参数
type: object
type: UniLabJsonCommand
auto-wait_complete:
auto-wait_for_completion:
feedback: {}
goal: {}
goal_default:
@@ -184,22 +535,23 @@ xyz_stepper_controller:
goal:
properties:
axis:
type: string
type: object
timeout:
default: 30.0
type: string
type: number
required:
- axis
type: object
result: {}
required:
- goal
title: wait_complete参数
title: wait_for_completion参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.laiyu_liquid_test.xyz_stepper_driver:XYZStepperController
module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController
status_types:
status: list
all_positions: dict
motor_status: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:MotorPosition
type: python
config_info: []
description: 新XYZ控制器
@@ -210,23 +562,24 @@ xyz_stepper_controller:
properties:
baudrate:
default: 115200
type: string
client:
type: string
origin_path:
default: unilabos/devices/laiyu_liquid_test/work_origin.json
type: string
type: integer
port:
default: /dev/ttyUSB0
type: string
required: []
timeout:
default: 1.0
type: number
required:
- port
type: object
data:
properties:
status:
type: array
all_positions:
type: object
motor_status:
type: object
required:
- status
- motor_status
- all_positions
type: object
registry_type: device
version: 1.0.0

View File

@@ -4019,8 +4019,7 @@ liquid_handler:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times:
- 0
mix_times: 0
mix_vol: 0
none_keys:
- ''
@@ -4176,11 +4175,9 @@ liquid_handler:
mix_stage:
type: string
mix_times:
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
maximum: 2147483647
minimum: -2147483648
type: integer
mix_vol:
maximum: 2147483647
minimum: -2147483648
@@ -5043,8 +5040,7 @@ liquid_handler.biomek:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times:
- 0
mix_times: 0
mix_vol: 0
none_keys:
- ''
@@ -5187,11 +5183,9 @@ liquid_handler.biomek:
mix_stage:
type: string
mix_times:
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
maximum: 2147483647
minimum: -2147483648
type: integer
mix_vol:
maximum: 2147483647
minimum: -2147483648
@@ -9284,7 +9278,13 @@ liquid_handler.prcxi:
z: 0.0
sample_id: ''
type: ''
handles: {}
handles:
input:
- data_key: wells
data_source: handle
data_type: resource
handler_key: input_wells
label: InputWells
placeholder_keys:
wells: unilabos_resources
result: {}
@@ -9671,8 +9671,7 @@ liquid_handler.prcxi:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times:
- 0
mix_times: 0
mix_vol: 0
none_keys:
- ''
@@ -9828,11 +9827,9 @@ liquid_handler.prcxi:
mix_stage:
type: string
mix_times:
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
maximum: 2147483647
minimum: -2147483648
type: integer
mix_vol:
maximum: 2147483647
minimum: -2147483648

View File

@@ -5,6 +5,73 @@ neware_battery_test_system:
- battery_test
class:
action_value_mappings:
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-print_status_summary:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: print_status_summary参数
type: object
type: UniLabJsonCommand
auto-test_connection:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: test_connection参数
type: object
type: UniLabJsonCommand
debug_resource_names:
feedback: {}
goal: {}
@@ -407,6 +474,8 @@ neware_battery_test_system:
status_types:
channel_status: dict
connection_info: dict
device_summary: dict
plate_status: dict
status: str
total_channels: int
type: python
@@ -418,36 +487,30 @@ neware_battery_test_system:
config:
properties:
devtype:
default: '27'
type: string
ip:
default: 127.0.0.1
type: string
machine_id:
default: 1
type: integer
oss_prefix:
default: neware_backup
description: OSS对象路径前缀
type: string
oss_upload_enabled:
default: false
description: 是否启用OSS上传功能
type: boolean
port:
default: 502
type: integer
size_x:
default: 500.0
default: 50
type: number
size_y:
default: 500.0
default: 50
type: number
size_z:
default: 2000.0
default: 20
type: number
timeout:
default: 20
type: integer
required: []
type: object
@@ -459,6 +522,8 @@ neware_battery_test_system:
type: object
device_summary:
type: object
plate_status:
type: object
status:
type: string
total_channels:
@@ -468,6 +533,7 @@ neware_battery_test_system:
- channel_status
- connection_info
- total_channels
- plate_status
- device_summary
type: object
version: 1.0.0

View File

@@ -3,6 +3,106 @@ post_process_station:
- post_process_station
class:
action_value_mappings:
auto-load_config:
feedback: {}
goal: {}
goal_default:
config_path: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
config_path:
type: string
required:
- config_path
type: object
result: {}
required:
- goal
title: load_config参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-print_cache_stats:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: print_cache_stats参数
type: object
type: UniLabJsonCommand
auto-set_node_value:
feedback: {}
goal: {}
goal_default:
name: null
value: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
name:
type: string
value:
type: string
required:
- name
- value
type: object
result: {}
required:
- goal
title: set_node_value参数
type: object
type: UniLabJsonCommand
disconnect:
feedback: {}
goal:
@@ -602,29 +702,46 @@ post_process_station:
type: SendCmd
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
status_types:
acetone_tank_empty_alarm: Bool
atomization_fast_speed: Float64
atomization_pressure_kpa: Int32
cleaning_complete: Bool
device_ready: Bool
door_open_alarm: Bool
grab_complete: Bool
grab_trigger: Bool
injection_pump_push_speed: Int32
injection_pump_suction_speed: Int32
nmp_tank_empty_alarm: Bool
post_process_complete: Bool
post_process_trigger: Bool
raw_tank_number: Int32
reaction_tank_number: Int32
remote_mode: Bool
wash_slow_speed: Float64
waste_tank_full_alarm: Bool
water_tank_empty_alarm: Bool
cache_stats: dict
node_value: String
type: python
config_info: []
description: 后处理站
handles: []
icon: post_process_station.webp
init_param_schema: {}
init_param_schema:
config:
properties:
cache_timeout:
default: 5.0
type: number
config_path:
type: string
deck:
type: string
password:
type: string
subscription_interval:
default: 500
type: integer
url:
type: string
use_subscription:
default: true
type: boolean
username:
type: string
required:
- url
type: object
data:
properties:
cache_stats:
type: object
node_value:
type: string
required:
- node_value
- cache_stats
type: object
version: 1.0.0

View File

@@ -4,6 +4,81 @@ reaction_station.bioyond:
- reaction_station_bioyond
class:
action_value_mappings:
add_time_constraint:
feedback: {}
goal:
duration: duration
end_point: end_point
end_step_key: end_step_key
start_point: start_point
start_step_key: start_step_key
goal_default:
duration: 0
end_point: 0
end_step_key: ''
start_point: 0
start_step_key: ''
handles: {}
result: {}
schema:
description: 添加时间约束 - 在两个工作流之间添加时间约束
properties:
feedback: {}
goal:
properties:
duration:
description: 时间(秒)
type: integer
end_point:
default: Start
description: 终点计时点 (Start=开始前, End=结束后)
enum:
- Start
- End
type: string
end_step_key:
description: 终点步骤Key (可选, 默认为空则自动选择)
type: string
start_point:
default: Start
description: 起点计时点 (Start=开始前, End=结束后)
enum:
- Start
- End
type: string
start_step_key:
description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择)
type: string
required:
- duration
type: object
result: {}
required:
- goal
title: add_time_constraint参数
type: object
type: UniLabJsonCommand
auto-clear_workflows:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: clear_workflows参数
type: object
type: UniLabJsonCommand
auto-create_order:
feedback: {}
goal: {}
@@ -131,6 +206,35 @@ reaction_station.bioyond:
title: process_web_workflows参数
type: object
type: UniLabJsonCommand
auto-set_reactor_temperature:
feedback: {}
goal: {}
goal_default:
reactor_id: null
temperature: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
reactor_id:
type: integer
temperature:
type: number
required:
- reactor_id
- temperature
type: object
result: {}
required:
- goal
title: set_reactor_temperature参数
type: object
type: UniLabJsonCommand
auto-skip_titration_steps:
feedback: {}
goal: {}
@@ -156,6 +260,27 @@ reaction_station.bioyond:
title: skip_titration_steps参数
type: object
type: UniLabJsonCommand
auto-sync_workflow_sequence_from_bioyond:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: sync_workflow_sequence_from_bioyond参数
type: object
type: UniLabJsonCommand
auto-wait_for_multiple_orders_and_get_reports:
feedback: {}
goal: {}
@@ -188,6 +313,33 @@ reaction_station.bioyond:
title: wait_for_multiple_orders_and_get_reports参数
type: object
type: UniLabJsonCommand
auto-workflow_sequence:
feedback: {}
goal: {}
goal_default:
value: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
value:
items:
type: string
type: array
required:
- value
type: object
result: {}
required:
- goal
title: workflow_sequence参数
type: object
type: UniLabJsonCommand
auto-workflow_step_query:
feedback: {}
goal: {}
@@ -213,6 +365,36 @@ reaction_station.bioyond:
title: workflow_step_query参数
type: object
type: UniLabJsonCommand
clean_all_server_workflows:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result:
code: code
message: message
schema:
description: 清空服务端所有非核心工作流 (保留核心流程)
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result:
properties:
code:
description: 操作结果代码(1表示成功)
type: integer
message:
description: 结果描述
type: string
type: object
required:
- goal
title: clean_all_server_workflows参数
type: object
type: UniLabJsonCommand
drip_back:
feedback: {}
goal:
@@ -247,13 +429,19 @@ reaction_station.bioyond:
description: 观察时间(分钟)
type: string
titration_type:
description: 是否滴定(1=否, 2=是)
description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
description: 是否观察 (NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string
volume:
description: 分液公式(μL)
description: 分液公式(mL)
type: string
required:
- volume
@@ -353,13 +541,19 @@ reaction_station.bioyond:
description: 观察时间(分钟)
type: string
titration_type:
description: 是否滴定(1=否, 2=是)
description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
description: 是否观察 (NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string
volume:
description: 分液公式(μL)
description: 分液公式(mL)
type: string
required:
- volume
@@ -403,7 +597,7 @@ reaction_station.bioyond:
label: Solvents Data From Calculation Node
result: {}
schema:
description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。
description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。
properties:
feedback: {}
goal:
@@ -423,15 +617,21 @@ reaction_station.bioyond:
description: 观察时间(分钟),默认360
type: string
titration_type:
default: '1'
description: 是否滴定(1=否, 2=是),默认1
default: 'NO'
description: 是否滴定(NO=否, YES=是),默认NO
enum:
- 'NO'
- 'YES'
type: string
torque_variation:
default: '2'
description: 是否观察 (1=否, 2=是),默认2
default: 'YES'
description: 是否观察 (NO=否, YES=是),默认YES
enum:
- 'NO'
- 'YES'
type: string
volume:
description: 分液量(μL)。可直接提供,或通过solvents参数自动计算
description: 分液量(mL)。可直接提供,或通过solvents参数自动计算
type: string
required:
- assign_material_name
@@ -504,15 +704,21 @@ reaction_station.bioyond:
description: 观察时间(分钟),默认90
type: string
titration_type:
default: '2'
description: 是否滴定(1=否, 2=是),默认2
default: 'YES'
description: 是否滴定(NO=否, YES=是),默认YES
enum:
- 'NO'
- 'YES'
type: string
torque_variation:
default: '2'
description: 是否观察 (1=否, 2=是),默认2
default: 'YES'
description: 是否观察 (NO=否, YES=是),默认YES
enum:
- 'NO'
- 'YES'
type: string
volume_formula:
description: 分液公式(μL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
type: string
x_value:
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
@@ -560,13 +766,19 @@ reaction_station.bioyond:
description: 观察时间(分钟)
type: string
titration_type:
description: 是否滴定(1=否, 2=是)
description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
description: 是否观察 (NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string
volume_formula:
description: 分液公式(μL)
description: 分液公式(mL)
type: string
required:
- volume_formula
@@ -680,6 +892,35 @@ reaction_station.bioyond:
title: reactor_taken_out参数
type: object
type: UniLabJsonCommand
scheduler_start:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result:
return_info: return_info
schema:
description: 启动调度器 - 启动Bioyond工作站的任务调度器开始执行队列中的任务
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result:
properties:
return_info:
description: 调度器启动结果成功返回1失败返回0
type: integer
required:
- return_info
title: scheduler_start结果
type: object
required:
- goal
title: scheduler_start参数
type: object
type: UniLabJsonCommand
solid_feeding_vials:
feedback: {}
goal:
@@ -706,7 +947,11 @@ reaction_station.bioyond:
description: 物料名称(用于获取试剂瓶位ID)
type: string
material_id:
description: 粉末类型ID1=盐21分钟2=面粉27分钟3=BTDA38分钟
description: 粉末类型IDSalt=盐21分钟Flour=面粉27分钟BTDA=BTDA38分钟
enum:
- Salt
- Flour
- BTDA
type: string
temperature:
description: 温度设定(°C)
@@ -715,7 +960,10 @@ reaction_station.bioyond:
description: 观察时间(分钟)
type: string
torque_variation:
description: 是否观察 (1=否, 2=是)
description: 是否观察 (NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string
required:
- assign_material_name
@@ -730,10 +978,10 @@ reaction_station.bioyond:
title: solid_feeding_vials参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactionStation
protocol_type: []
status_types:
workflow_sequence: String
workflow_sequence: str
type: python
config_info: []
description: Bioyond反应站
@@ -753,9 +1001,7 @@ reaction_station.bioyond:
data:
properties:
workflow_sequence:
items:
type: string
type: array
type: string
required:
- workflow_sequence
type: object
@@ -791,7 +1037,7 @@ reaction_station.reactor:
title: update_metrics参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactor
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactor
status_types: {}
type: python
config_info: []

View File

@@ -124,11 +124,25 @@ class Registry:
"output": [
{
"handler_key": "labware",
"label": "Labware",
"data_type": "resource",
"data_source": "handle",
"data_key": "liquid",
}
"label": "Labware",
"data_source": "executor",
"data_key": "created_resource_tree.@flatten",
},
{
"handler_key": "liquid_slots",
"data_type": "resource",
"label": "LiquidSlots",
"data_source": "executor",
"data_key": "liquid_input_resource_tree.@flatten",
},
{
"handler_key": "materials",
"data_type": "resource",
"label": "AllMaterials",
"data_source": "executor",
"data_key": "[created_resource_tree,liquid_input_resource_tree].@flatten.@flatten",
},
]
},
"placeholder_keys": {
@@ -186,7 +200,17 @@ class Registry:
"resources": "unilabos_resources",
},
"goal_default": {},
"handles": {},
"handles": {
"input": [
{
"handler_key": "input_resources",
"data_type": "resource",
"label": "InputResources",
"data_source": "handle",
"data_key": "resources", # 不为空
},
]
},
},
},
},
@@ -455,7 +479,11 @@ class Registry:
return status_schema
def _generate_unilab_json_command_schema(
self, method_args: List[Dict[str, Any]], method_name: str, return_annotation: Any = None
self,
method_args: List[Dict[str, Any]],
method_name: str,
return_annotation: Any = None,
previous_schema: Dict[str, Any] | None = None,
) -> Dict[str, Any]:
"""
根据UniLabJsonCommand方法信息生成JSON Schema暂不支持嵌套类型
@@ -464,6 +492,7 @@ class Registry:
method_args: 方法信息字典包含args等
method_name: 方法名称
return_annotation: 返回类型注解用于生成result schema仅支持TypedDict
previous_schema: 之前的 schema用于保留 goal/feedback/result 下一级字段的 description
Returns:
JSON Schema格式的参数schema
@@ -497,7 +526,7 @@ class Registry:
if return_annotation is not None and self._is_typed_dict(return_annotation):
result_schema = self._generate_typed_dict_result_schema(return_annotation)
return {
final_schema = {
"title": f"{method_name}参数",
"description": f"",
"type": "object",
@@ -505,6 +534,39 @@ class Registry:
"required": ["goal"],
}
# 保留之前 schema 中 goal/feedback/result 下一级字段的 description
if previous_schema:
self._preserve_field_descriptions(final_schema, previous_schema)
return final_schema
def _preserve_field_descriptions(
self, new_schema: Dict[str, Any], previous_schema: Dict[str, Any]
) -> None:
"""
保留之前 schema 中 goal/feedback/result 下一级字段的 description
Args:
new_schema: 新生成的 schema会被修改
previous_schema: 之前的 schema
"""
for section in ["goal", "feedback", "result"]:
new_section = new_schema.get("properties", {}).get(section, {})
prev_section = previous_schema.get("properties", {}).get(section, {})
if not new_section or not prev_section:
continue
new_props = new_section.get("properties", {})
prev_props = prev_section.get("properties", {})
for field_name, field_schema in new_props.items():
if field_name in prev_props:
prev_field = prev_props[field_name]
# 保留字段的 description
if "description" in prev_field and prev_field["description"]:
field_schema["description"] = prev_field["description"]
def _is_typed_dict(self, annotation: Any) -> bool:
"""
检查类型注解是否是TypedDict
@@ -673,13 +735,10 @@ class Registry:
sorted(device_config["class"]["status_types"].items())
)
if complete_registry:
# 保存原有的description信息
old_descriptions = {}
# 保存原有的 action 配置(用于保留 schema 的 description 和 handles 等)
old_action_configs = {}
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
if "description" in action_config.get("schema", {}):
description = action_config["schema"]["description"]
if len(description):
old_descriptions[action_name] = action_config["schema"]["description"]
old_action_configs[action_name] = action_config
device_config["class"]["action_value_mappings"] = {
k: v
@@ -695,10 +754,15 @@ class Registry:
"feedback": {},
"result": {},
"schema": self._generate_unilab_json_command_schema(
v["args"], k, v.get("return_annotation")
v["args"],
k,
v.get("return_annotation"),
# 传入旧的 schema 以保留字段 description
old_action_configs.get(f"auto-{k}", {}).get("schema"),
),
"goal_default": {i["name"]: i["default"] for i in v["args"]},
"handles": [],
# 保留原有的 handles 配置
"handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []),
"placeholder_keys": {
i["name"]: (
"unilabos_resources"
@@ -722,12 +786,14 @@ class Registry:
if k not in device_config["class"]["action_value_mappings"]
}
)
# 恢复原有的description信息auto开头的不修改
for action_name, description in old_descriptions.items():
# 恢复原有的 description 信息(auto- 开头的动作
for action_name, old_config in old_action_configs.items():
if action_name in device_config["class"]["action_value_mappings"]: # 有一些会被删除
device_config["class"]["action_value_mappings"][action_name]["schema"][
"description"
] = description
old_schema = old_config.get("schema", {})
if "description" in old_schema and old_schema["description"]:
device_config["class"]["action_value_mappings"][action_name]["schema"][
"description"
] = old_schema["description"]
device_config["init_param_schema"] = {}
device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema(
enhanced_info["init_params"], "__init__"

View File

@@ -0,0 +1,170 @@
# UniLabOS 资源注册架构详解
> **目标受众**: 主要开发 `unilabos/registry/devices` 抽象层的开发者
> **最后更新**: 2026-01-11
> **维护者**: Uni-Lab-OS 开发团队
本文档详细说明 UniLabOS 资源注册系统的架构、资源的完整生命周期,以及如何实现动态物料位置追踪。
---
## 📚 目录
- [核心概念](#核心概念)
- [三层架构详解](#三层架构详解)
- [资源注册机制](#资源注册机制)
- [物料生命周期管理](#物料生命周期管理)
- [动态物料位置追踪](#动态物料位置追踪)
- [实战案例](#实战案例)
- [常见问题排查](#常见问题排查)
---
## 核心概念
### 1. Resources vs Registry
UniLabOS 采用**声明式注册**模式,将资源的**定义**Python与**注册信息**YAML分离
```
┌──────────────────────────────────────────────────────────┐
│ unilabos/resources (Python 实现) │
│ - 定义资源的物理属性、行为和创建逻辑 │
│ - 例如: 瓶子的尺寸、容量、材质 │
├──────────────────────────────────────────────────────────┤
│ unilabos/registry/resources (YAML 注册表) │
│ - 声明哪些资源可以被前端使用 │
│ - 定义资源的分类、图标、初始化参数 │
└──────────────────────────────────────────────────────────┘
```
**为什么要分离?**
1. **解耦**: Python 代码可以定义无限多的资源类,但只有在 YAML 中注册的才能被前端识别
2. **灵活性**: 无需修改 Python 代码,只需修改 YAML 就能添加/移除可用资源
3. **可扩展性**: 第三方开发者可以通过 YAML 注册自己的资源,无需修改核心代码
---
## 三层架构详解
UniLabOS 资源系统采用**三层架构**实现从前端UI到底层硬件的完整映射
### 架构图
```
┌─────────────────────────────────────────────────────┐
│ 第1层: YAML 注册表 (registry/resources) │
│ - 告诉系统"哪些资源可用" │
│ - 前端通过此层获取可用资源列表 │
│ - 文件: YB_bottle.yaml, YB_bottle_carriers.yaml │
├─────────────────────────────────────────────────────┤
│ 第2层: Python 实现 (resources/bioyond) │
│ - 定义资源的具体属性和行为 │
│ - 创建资源实例的工厂函数 │
│ - 文件: YB_bottles.py, YB_bottle_carriers.py │
├─────────────────────────────────────────────────────┤
│ 第3层: Hardware/API 集成 (devices/workstation) │
│ - 连接 Bioyond 系统 API │
│ - 同步物料位置和状态 │
│ - 文件: station.py, bioyond_rpc.py, config.py │
└─────────────────────────────────────────────────────┘
```
### 第1层: YAML 注册表
#### YB_bottle.yaml - 单个瓶子注册
```yaml
YB_5ml_fenyeping:
category:
- yb3 # 系统分类
- YB_bottle # 资源类型
class:
module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping # Python 函数路径
type: pylabrobot # 框架类型
description: YB_5ml_fenyeping # 前端显示名称
handles: []
icon: '' # 图标路径
init_param_schema: {} # 初始化参数 schema
registry_type: resource
version: 1.0.0
```
**作用:**
- 前端通过读取此文件知道有一个叫 "YB_5ml_fenyeping" 的资源
- 用户拖拽时,系统会调用 `YB_bottles:YB_5ml_fenyeping()` 创建实例
#### YB_bottle_carriers.yaml - 载架(容器)注册
```yaml
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 # 5ml分液瓶板
```
**作用:**
- 载架是容器,里面可以放多个瓶子
- 例如: `YB_5ml_fenyepingban` 是一个 4x2 布局的板,可以放 8 个 5ml 瓶子
### 第2层: Python 实现
#### YB_bottles.py - 瓶子工厂函数
```python
def YB_5ml_fenyeping(
name: str,
diameter: float = 20.0, # 直径 (mm)
height: float = 50.0, # 高度 (mm)
max_volume: float = 5000.0, # 最大容量 (μL)
barcode: str = None,
) -> Bottle:
\"\"\"创建5ml分液瓶\"\"\"
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_5ml_fenyeping", # ⭐ 与 YAML 中的名称对应
)
```
**关键点:**
- 函数名 `YB_5ml_fenyeping` 必须与 YAML 中的 `module` 路径末尾一致
- 返回一个 `Bottle` 对象PyLabRobot 资源类型)
- `model` 字段用于在 Bioyond 系统中识别资源类型
**详细文档请参考完整版 README**
---
## 相关文件索引
### 核心文件
| 文件 | 功能 | 路径 |
|------|------|------|
| `YB_bottle.yaml` | 瓶子注册表 | `unilabos/registry/resources/bioyond/` |
| `YB_bottle_carriers.yaml` | 载架注册表 | `unilabos/registry/resources/bioyond/` |
| `deck.yaml` | Deck注册表 | `unilabos/registry/resources/bioyond/` |
| `YB_bottles.py` | 瓶子实现 | `unilabos/resources/bioyond/` |
| `YB_bottle_carriers.py` | 载架实现 | `unilabos/resources/bioyond/` |
| `YB_warehouses.py` | 仓库实现 | `unilabos/resources/bioyond/` |
| `decks.py` | Deck布局 | `unilabos/resources/bioyond/` |
| `station.py` | 物料同步 | `unilabos/devices/workstation/bioyond_studio/` |
| `config.py` | UUID映射 | `unilabos/devices/workstation/bioyond_studio/` |
### 仓库相关文档
- [README_WAREHOUSE.md](../../resources/bioyond/README_WAREHOUSE.md) - 仓库系统开发指南
---
**维护者:** Uni-Lab-OS 开发团队
**最后更新:** 2026-01-11

View File

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

View File

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

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