Compare commits

..

74 Commits

Author SHA1 Message Date
Xuwznln
a277bd2bed CI Check use production mode 2026-01-27 19:59:22 +08:00
Xuwznln
176de521b4 v0.10.17 2026-01-27 19:41:12 +08:00
dependabot[bot]
38c5c267af Fix Conda Build
ci(deps): bump actions/checkout from 4 to 6 (#223)

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

ci(deps): bump actions/upload-pages-artifact from 3 to 4 (#225)

Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

ci(deps): bump actions/upload-artifact from 4 to 6 (#224)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

ci(deps): bump actions/configure-pages from 4 to 5 (#222)

Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 4 to 5.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-27 19:39:42 +08:00
Xuwznln
2a5ddd611d Upgrade to py 3.11.14; ROS2 Humble 0.7; unilabos 0.10.16
Workbench example, adjust log level, and ci check (#220)

* TestLatency Return Value Example & gitignore update

* Adjust log level & Add workbench virtual example & Add not action decorator & Add check_mode &

* Add CI Check

Fix/workstation yb revision (#217)

* Revert log change & update registry

* Revert opcua client & move electrolyte node

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>

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

fix parent_uuid fetch when bind_parent_id == node_name

物料更新也是用父节点进行报送

Add None conversion for tube rack etc.

Add set_liquid example.

Add create_resource and test_resource example.

Add restart.
Temp allow action message.

Add no_update_feedback option.

Create session_id by edge.

bump version to 0.10.15

temp cancel update req
2026-01-27 15:21:55 +08:00
Xuwznln
8580b84167 Fix update with different spot and same parent 2026-01-08 03:46:44 +08:00
Xuwznln
3f80349d7d Force update resource when adding new resource / transfer to another resource
location not passed to ItemizedCarrier when assign child resource

Fix size not pass through.
2026-01-08 03:08:01 +08:00
Xuwznln
024156848e Fix build on macos-intel 2026-01-07 21:11:33 +08:00
Xuwznln
8066c200b9 Update README.md
Modify resource_tracker file module path.
2026-01-07 20:54:20 +08:00
Xuwznln
266366cc25 Bump version to 0.10.4 2026-01-07 20:46:23 +08:00
Xuwznln
121c3985cc Update LICENSE 2026-01-07 20:46:23 +08:00
Xuwznln
6ca5c72fc6 Fix drag materials.
Fix and tested new create_resource.

Update create_resource to resource tree mode.
2026-01-07 20:46:23 +08:00
Xianwei Qi
bc8c49ddda test_transfer_liquid 2026-01-07 20:45:41 +08:00
Xuwznln
28f93737ac Close #208. Fix mock devices. 2025-12-28 23:22:50 +08:00
ZiWei
5dc81ec9be bump version to 0.10.3
update registry

do not modify axis globally

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>

Add topic config

add camera driver (#191)

* add camera driver

* add init.py file to cameraSII driver

增强新威电池测试系统 OSS 上传功能 / Enhanced Neware Battery Test System OSS Upload (#196)

* feat: neware-oss-upload-enhancement

* feat(neware): enhance OSS upload with metadata and workflow handles

Add post process station and related resources (#195)

* Add post process station and related resources

- Created JSON configuration for post_process_station and its child post_process_deck.
- Added YAML definitions for post_process_station, bottle carriers, bottles, and deck resources.
- Implemented Python classes for bottle carriers, bottles, decks, and warehouses to manage resources in the post process.
- Established a factory method for creating warehouses with customizable dimensions and layouts.
- Defined the structure and behavior of the post_process_deck and its associated warehouses.

* feat(post_process): add post_process_station and related warehouse functionality

- Introduced post_process_station.json to define the post-processing station structure.
- Implemented post_process_warehouse.py to create warehouse configurations with customizable layouts.
- Added warehouses.py for specific warehouse configurations (4x3x1).
- Updated post_process_station.yaml to reflect new module paths for OpcUaClient.
- Refactored bottle carriers and bottles YAML files to point to the new module paths.
- Adjusted deck.yaml to align with the new organizational structure for post_process_deck.

prcxi resource (#202)

* prcxi resource

* prcxi_resource

* Fix upload error not showing.
Support str type category.

---------

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

Fix upload error not showing.
Support str type category.

feat: introduce `wait_time` command and configurable device communication timeout.

feat: Add `SyringePump` (SY-03B) driver with unified serial/TCP transport for `chinwe` device, including registry and test configurations.
2025-12-26 03:36:48 +08:00
Xuwznln
13a6795657 Update organic syn station. 2025-12-15 02:34:36 +08:00
Xianwei Qi
53219d8b04 Update docs
update "laiyu" missing init file.

fix "laiyu" missing init file.

fix "🐛 fix"

🐛 fix: config file is overwrited by default args even if not be set.

mix

修改了mix,仿真流程报错问题
2025-12-14 13:13:21 +08:00
Xuwznln
b1cdef9185 update version to 0.10.12 2025-12-04 18:47:16 +08:00
Xuwznln
9854ed8c9c fix ros2 future
print all logs to file
fix resource dict dump error
2025-12-04 18:46:37 +08:00
Xuwznln
52544a2c69 signal when host node is ready 2025-12-02 12:00:26 +08:00
ZiWei
5ce433e235 Fix startup with remote resource error
Resource dict fully change to "pose" key

Update oss link

Reduce pylabrobot conversion warning & force enable log dump.

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

* Updated 9320 host address

* Updated 9320 host address

* Add **kwargs in classes: PRCXI9300Deck and PRCXI9300Container

* Removed all sample_id in prcxi_9320.json to avoid KeyError

* 9320 machine testing settings

* Typo

* Typo in base_device_node.py

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

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

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

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

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

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

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

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

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

* Refactor Bioyond resource synchronization and update bottle carrier definitions

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

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

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

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

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

* support name change during materials change

* fix json dumps

* correct tip

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

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

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

* 修复 ItemizedCarrier 中的可见性逻辑

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

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

* Fix bioyond bottle_carriers ordering

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

* disable slave connect websocket

* correct remove_resource stats

* change uuid logger to trace level

* enable slave mode

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* refactor(dispensing_station): 移除wait_for_order_completion_and_get_report功能

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

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

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

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

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

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

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

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

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

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

---------

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

* remove redundant arm_slider meshes

---------

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

* 修改lh的json启动

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

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

* 添加laiyu硬件连接

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

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

* 修改laiyu移液站

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

* 更新lh以及laiyu workshop

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

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

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

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

* 修改枪头动作

* 修改虚拟仿真方法

---------

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

* 构建物料教程byxinyu

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

* Updated 9320 host address

* Updated 9320 host address

* Add **kwargs in classes: PRCXI9300Deck and PRCXI9300Container

* Removed all sample_id in prcxi_9320.json to avoid KeyError

* 9320 machine testing settings

* Typo

* Rewrite setup logic to clear error code

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

prcxi example

fix append_resource

fix regularcontainer

fix cancel error

fix resource_get param

fix json dumps

support name change during materials change

enable slave mode

change uuid logger to trace level

correct remove_resource stats

disable slave connect websocket

adjust with_children param

modify devices to use correct executor (sleep, create_task)

support sleep and create_task in node

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

bioyond_HR (#133)

* feat: Enhance Bioyond synchronization and resource management

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

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

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

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

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

Add batch creation methods for vial and solution tasks

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

convert identifier to site idx

correct extra key

update extra before transfer

fix multiple instance error

add resource_tree_transfer func

fox itemrized carrier assign child resource

support internal device material transfer

remove extra key

use same callback group

support material extra

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

prcxi样例图

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

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

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

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

add create_workflow script and test

add invisible_slots to carriers

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

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

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

More decks&plates support for bioyond (#115)

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

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

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

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

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

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

fix warehouse serialize/deserialize

fix bioyond converter

fix itemized_carrier.unassign_child_resource

allow not-loaded MSG in registry

add layout serializer & converter

warehouseuse A1-D4; add warehouse layout

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

Fix resource assignment and type mapping issues

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

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

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

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

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

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

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

* correct return message

---------

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

* Raise error when using unsupported type to create ResourceTreeSet

* Fix children key error

* Fix children key error

* Fix workstation resource not tracking

* Fix workstation deck & children resource dupe

* Fix workstation deck & children resource dupe

* Fix multiple resource error

* Fix resource tree update

* Fix resource tree update

* Force confirm uuid

* Tip more error log

* Refactor Bioyond workstation and experiment workflow (#105)

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

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

* mount parent uuid

* Add logging configuration based on BasicConfig in main function

* fix workstation node error

* fix workstation node error

* Update boot example

* temp fix for resource get

* temp fix for resource get

* provide error info when cant find plr type

* pack repo info

* fix to plr type error

* fix to plr type error

* Update regular container method

* support no size init

* fix comprehensive_station.json

* fix comprehensive_station.json

* fix type conversion

* fix state loading for regular container

* Update deploy-docs.yml

* Update deploy-docs.yml

---------

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

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

Fix param error when using mamba run

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

* delete deprecated mock devices

* rename categories

* combine chromatographic devices

* rename rviz simulation nodes

* organic virtual devices

* parse vessel_id

* run registry completion before merge

---------

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

* fix: workstation handlers and vessel_id parsing

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

* modify default discovery_interval to 15s

* feat: add trace log level

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

* fix: drop_tips not using auto resource select

* fix: discard_tips error

* fix: discard_tips

* fix: prcxi_res

* add: prcxi res
fix: startup slow

* feat: workstation example

* fix pumps and liquid_handler handle

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

* fix all protocol_compilers and remove deprecated devices

* feat: 新增use_remote_resource参数

* fix and remove redundant info

* bugfixes on organic protocols

* fix filter protocol

* fix protocol node

* 临时兼容错误的driver写法

* fix: prcxi import error

* use call_async in all service to avoid deadlock

* fix: figure_resource

* Update recipe.yaml

* add workstation template and battery example

* feat: add sk & ak

* update workstation base

* Create workstation_architecture.md

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

* refactor: ProtocolNode→WorkstationNode

* Add:msgs.action (#83)

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

* Add:msgs.action

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

* simplify resource system

* uncompleted refactor

* example for use WorkstationBase

* feat: websocket

* feat: websocket test

* feat: workstation example

* feat: action status

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

* fix: 还原protocol node处理方法

* fix: build

* fix: missing job_id key

* ws test version 1

* ws test version 2

* ws protocol

* 增加物料关系上传日志

* 增加物料关系上传日志

* 修正物料关系上传

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

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

* 修复event loop错误

* 修复edge上报错误

* 修复async错误

* 更新schema的title字段

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

* 注册表编辑器

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

* 增加addr参数

* fix: addr param

* fix: addr param

* 取消labid 和 强制config输入

* Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup

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

* Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists

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

* result_info改为字典类型

* 新增uat的地址替换

* runze multiple pump support

(cherry picked from commit 49354fcf39)

* remove runze multiple software obtainer

(cherry picked from commit 8bcc92a394)

* support multiple backbone

(cherry picked from commit 4771ff2347)

* Update runze pump format

* Correct runze multiple backbone

* Update runze_multiple_backbone

* Correct runze pump multiple receive method.

* Correct runze pump multiple receive method.

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

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

* fix import error

* fix dupe upload registry

* refactor ws client

* add server timeout

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

* fix run_column

* Update run_column_protocol.py

(cherry picked from commit e5aa4d940a)

* resource_update use resource_add

* 新增版位推荐功能

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

* update registry with nested obj

* fix protocol node log_message, added create_resource return value

* fix protocol node log_message, added create_resource return value

* try fix add protocol

* fix resource_add

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

* Feature/xprbalance-zhida (#80)

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

* feat(devices): add mettler_toledo xpr balance

* balance

* 重新补全zhida注册表

* PRCXI9320 json

* PRCXI9320 json

* PRCXI9320 json

* fix resource download

* remove class for resource

* bump version to 0.10.6

* 更新所有注册表

* 修复protocolnode的兼容性

* 修复protocolnode的兼容性

* Update install md

* Add Defaultlayout

* 更新物料接口

* fix dict to tree/nested-dict converter

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

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

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

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

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

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

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

* frontend_docs

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

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

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

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

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

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

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

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

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

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

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

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

---------

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

* 更新物料接口

* Workstation dev yb2 (#100)

* Refactor and extend reaction station action messages

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

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

* 修复to_plr_resources

* add update remove

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

* 修复资源添加

* 修复transfer_resource_to_another生成

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

* 新增test_resource动作

* fix host_node error

* fix host_node test_resource error

* fix host_node test_resource error

* 过滤本地动作

* 移动内部action以兼容host node

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

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

* update todo

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

* pass the tests

* update todo

* add conda-pack-build.yml

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

(cherry picked from commit 172599adcf)

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

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

* Update conda-pack-build.yml

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

* Update conda-pack-build.yml

* Fix FileNotFoundError

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

* Fix unilabos msgs search error

* Fix environment_check.py

* Update recipe.yaml

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

* Fix nested conda pack

* Fix one-key installation path error

* Bump version to 0.10.7

* Workshop bj (#99)

* Add LaiYu Liquid device integration and tests

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

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

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

* add

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

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

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

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

* 修复大小写文件夹名字

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

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

* Update intro.md

* 物料教程

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

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

* Update prcxi driver & fix transfer_liquid mix_times

* fix: correct mix_times type

* Update liquid_handler registry

* test: prcxi.py

* Update registry from pr

* fix ony-key script not exist

* clean files

---------

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

View File

@@ -3,7 +3,7 @@
package:
name: unilabos
version: 0.10.19
version: 0.10.17
source:
path: ../../unilabos
@@ -41,20 +41,19 @@ requirements:
- networkx
- typing_extensions
- websockets
- opentrons_shared_data
- pint
- fastapi
- jinja2
- requests
- uvicorn
- if: not osx
then:
- opcua
- opcua
- pyserial
- pandas
- pymodbus
- matplotlib
- pylibftdi
- uni-lab::unilabos-env ==0.10.19
- uni-lab::unilabos-env ==0.10.17
about:
repository: https://github.com/deepmodeling/Uni-Lab-OS

View File

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

View File

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

View File

@@ -1,160 +0,0 @@
---
name: add-device
description: Guide for adding new devices to Uni-Lab-OS (接入新设备). Uses @device decorator + AST auto-scanning instead of manual YAML. Walks through device category, communication protocol, driver creation with decorators, and graph file setup. Use when the user wants to add/integrate a new device, create a device driver, write a device class, or mentions 接入设备/添加设备/设备驱动/物模型.
---
# 添加新设备到 Uni-Lab-OS
**第一步:** 使用 Read 工具读取 `docs/ai_guides/add_device.md`,获取完整的设备接入指南。
该指南包含设备类别(物模型)列表、通信协议模板、常见错误检查清单等。搜索 `unilabos/devices/` 获取已有设备的实现参考。
---
## 装饰器参考
### @device — 设备类装饰器
```python
from unilabos.registry.decorators import device
# 单设备
@device(
id="my_device.vendor", # 注册表唯一标识(必填)
category=["temperature"], # 分类标签列表(必填)
description="设备描述", # 设备描述
display_name="显示名称", # UI 显示名称(默认用 id
icon="DeviceIcon.webp", # 图标文件名
version="1.0.0", # 版本号
device_type="python", # "python" 或 "ros2"
handles=[...], # 端口列表InputHandle / OutputHandle
model={...}, # 3D 模型配置
hardware_interface=HardwareInterface(...), # 硬件通信接口
)
# 多设备(同一个类注册多个设备 ID各自有不同的 handles 等配置)
@device(
ids=["pump.vendor.model_A", "pump.vendor.model_B"],
id_meta={
"pump.vendor.model_A": {"handles": [...], "description": "型号 A"},
"pump.vendor.model_B": {"handles": [...], "description": "型号 B"},
},
category=["pump_and_valve"],
)
```
### @action — 动作方法装饰器
```python
from unilabos.registry.decorators import action
@action # 无参:注册为 UniLabJsonCommand 动作
@action() # 同上
@action(description="执行操作") # 带描述
@action(
action_type=HeatChill, # 指定 ROS Action 消息类型
goal={"temperature": "temp"}, # Goal 字段映射
feedback={}, # Feedback 字段映射
result={}, # Result 字段映射
handles=[...], # 动作级别端口
goal_default={"temp": 25.0}, # Goal 默认值
placeholder_keys={...}, # 参数占位符
always_free=True, # 不受排队限制
auto_prefix=True, # 强制使用 auto- 前缀
parent=True, # 从父类 MRO 获取参数签名
)
```
**自动识别规则:**
-`@action` 的公开方法 → 注册为动作(方法名即动作名)
- **不带 `@action` 的公开方法** → 自动注册为 `auto-{方法名}` 动作
- `_` 开头的方法 → 不扫描
- `@not_action` 标记的方法 → 排除
### @topic_config — 状态属性配置
```python
from unilabos.registry.decorators import topic_config
@property
@topic_config(
period=5.0, # 发布周期(秒),默认 5.0
print_publish=False, # 是否打印发布日志
qos=10, # QoS 深度,默认 10
name="custom_name", # 自定义发布名称(默认用属性名)
)
def temperature(self) -> float:
return self.data.get("temperature", 0.0)
```
### 辅助装饰器
```python
from unilabos.registry.decorators import not_action, always_free
@not_action # 标记为非动作post_init、辅助方法等
@always_free # 标记为不受排队限制(查询类操作)
```
---
## 设备模板
```python
import logging
from typing import Any, Dict, Optional
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
from unilabos.registry.decorators import device, action, topic_config, not_action
@device(id="my_device", category=["my_category"], description="设备描述")
class MyDevice:
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
self.device_id = device_id or "my_device"
self.config = config or {}
self.logger = logging.getLogger(f"MyDevice.{self.device_id}")
self.data: Dict[str, Any] = {"status": "Idle"}
@not_action
def post_init(self, ros_node: BaseROS2DeviceNode) -> None:
self._ros_node = ros_node
@action
async def initialize(self) -> bool:
self.data["status"] = "Ready"
return True
@action
async def cleanup(self) -> bool:
self.data["status"] = "Offline"
return True
@action(description="执行操作")
def my_action(self, param: float = 0.0, name: str = "") -> Dict[str, Any]:
"""带 @action 装饰器 → 注册为 'my_action' 动作"""
return {"success": True}
def get_info(self) -> Dict[str, Any]:
"""无 @action → 自动注册为 'auto-get_info' 动作"""
return {"device_id": self.device_id}
@property
@topic_config()
def status(self) -> str:
return self.data.get("status", "Idle")
@property
@topic_config(period=2.0)
def temperature(self) -> float:
return self.data.get("temperature", 0.0)
```
### 要点
- `_ros_node: BaseROS2DeviceNode` 类型标注放在类体顶部
- `__init__` 签名固定为 `(self, device_id=None, config=None, **kwargs)`
- `post_init``@not_action` 标记,参数类型标注为 `BaseROS2DeviceNode`
- 运行时状态存储在 `self.data` 字典中
- 设备文件放在 `unilabos/devices/<category>/` 目录下

View File

@@ -1,351 +0,0 @@
---
name: add-resource
description: Guide for adding new resources (materials, bottles, carriers, decks, warehouses) to Uni-Lab-OS (添加新物料/资源). Uses @resource decorator for AST auto-scanning. Covers Bottle, Carrier, Deck, WareHouse definitions. Use when the user wants to add resources, define materials, create a deck layout, add bottles/carriers/plates, or mentions 物料/资源/resource/bottle/carrier/deck/plate/warehouse.
---
# 添加新物料资源
Uni-Lab-OS 的资源体系基于 PyLabRobot通过扩展实现 Bottle、Carrier、WareHouse、Deck 等实验室物料管理。使用 `@resource` 装饰器注册AST 自动扫描生成注册表条目。
---
## 资源类型
| 类型 | 基类 | 用途 | 示例 |
|------|------|------|------|
| **Bottle** | `Well` (PyLabRobot) | 单个容器(瓶、小瓶、烧杯、反应器) | 试剂瓶、粉末瓶 |
| **BottleCarrier** | `ItemizedCarrier` | 多槽位载架(放多个 Bottle | 6 位试剂架、枪头盒 |
| **WareHouse** | `ItemizedCarrier` | 堆栈/仓库(放多个 Carrier | 4x4 堆栈 |
| **Deck** | `Deck` (PyLabRobot) | 工作站台面(放多个 WareHouse | 反应站 Deck |
**层级关系:** `Deck``WareHouse``BottleCarrier``Bottle`
WareHouse 本质上和 Site 是同一概念 — 都是定义一组固定的放置位slot只不过 WareHouse 多嵌套了一层 Deck。两者都需要开发者根据实际物理尺寸自行计算各 slot 的偏移坐标。
---
## @resource 装饰器
```python
from unilabos.registry.decorators import resource
@resource(
id="my_resource_id", # 注册表唯一标识(必填)
category=["bottles"], # 分类标签列表(必填)
description="资源描述",
icon="", # 图标
version="1.0.0",
handles=[...], # 端口列表InputHandle / OutputHandle
model={...}, # 3D 模型配置
class_type="pylabrobot", # "python" / "pylabrobot" / "unilabos"
)
```
---
## 创建规范
### 命名规则
1. **`name` 参数作为前缀**:所有工厂函数必须接受 `name: str` 参数,创建子物料时以 `name` 作为前缀,确保实例名在运行时全局唯一
2. **Bottle 命名约定**:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
3. **函数名 = `@resource(id=...)`**:工厂函数名与注册表 id 保持一致
### 子物料命名示例
```python
# Carrier 内部的 sites 用 name 前缀
for k, v in sites.items():
v.name = f"{name}_{v.name}" # "堆栈1左_A01", "堆栈1左_B02" ...
# Carrier 中放置 Bottle 时用 name 前缀
carrier[0] = My_Reagent_Bottle(f"{name}_flask_1") # "堆栈1左_flask_1"
carrier[i] = My_Solid_Vial(f"{name}_vial_{ordering[i]}") # "堆栈1左_vial_A1"
# create_homogeneous_resources 使用 name_prefix
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[...],
name_prefix=name, # 自动生成 "{name}_0", "{name}_1" ...
)
# Deck setup 中用仓库名称作为 name 传入
self.warehouses = {
"堆栈1左": my_warehouse_4x4("堆栈1左"), # WareHouse.name = "堆栈1左"
"试剂堆栈": my_reagent_stack("试剂堆栈"), # WareHouse.name = "试剂堆栈"
}
```
### 其他规范
- **max_volume 单位为 μL**500mL = 500000
- **尺寸单位为 mm**`diameter`, `height`, `size_x/y/z`, `dx/dy/dz`
- **BottleCarrier 必须设置 `num_items_x/y/z`**:用于前端渲染布局
- **Deck 的 `__init__` 必须接受 `setup=False`**:图文件中 `config.setup=true` 触发 `setup()`
- **按项目分组文件**:同一工作站的资源放在 `unilabos/resources/<project>/`
- **`__init__` 必须接受 `serialize()` 输出的所有字段**`serialize()` 输出会作为 `config` 回传到 `__init__`,因此必须通过显式参数或 `**kwargs` 接受,否则反序列化会报错
- **持久化运行时状态用 `serialize_state()`**:通过 `_unilabos_state` 字典存储可变信息(如物料内容、液体量),只存 JSON 可序列化的基本类型
---
## 资源模板
### Bottle
```python
from unilabos.registry.decorators import resource
from unilabos.resources.itemized_carrier import Bottle
@resource(id="My_Reagent_Bottle", category=["bottles"], description="我的试剂瓶")
def My_Reagent_Bottle(
name: str,
diameter: float = 70.0,
height: float = 120.0,
max_volume: float = 500000.0,
barcode: str = None,
) -> Bottle:
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="My_Reagent_Bottle",
)
```
**Bottle 参数:**
- `name`: 实例名称(运行时唯一,由上层 Carrier 以前缀方式传入)
- `diameter`: 瓶体直径 (mm)
- `height`: 瓶体高度 (mm)
- `max_volume`: 最大容积(**μL**500mL = 500000
- `barcode`: 条形码(可选)
### BottleCarrier
```python
from pylabrobot.resources import ResourceHolder
from pylabrobot.resources.carrier import create_ordered_items_2d
from unilabos.resources.itemized_carrier import BottleCarrier
from unilabos.registry.decorators import resource
@resource(id="My_6SlotCarrier", category=["bottle_carriers"], description="六槽位载架")
def My_6SlotCarrier(name: str) -> BottleCarrier:
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3, num_items_y=2,
dx=10.0, dy=10.0, dz=5.0,
item_dx=42.0, item_dy=35.0,
size_x=20.0, size_y=20.0, size_z=50.0,
)
# 子 site 用 name 作为前缀
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name, size_x=146.0, size_y=80.0, size_z=55.0,
sites=sites, model="My_6SlotCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
# 放置 Bottle 时用 name 作为前缀
ordering = ["A1", "B1", "A2", "B2", "A3", "B3"]
for i in range(6):
carrier[i] = My_Reagent_Bottle(f"{name}_vial_{ordering[i]}")
return carrier
```
### WareHouse / Deck 放置位
WareHouse 和 Site 本质上是同一概念都是定义一组固定放置位slot根据物理尺寸自行批量计算偏移坐标。WareHouse 只是多嵌套了一层 Deck 而已。推荐开发者直接根据实物测量数据计算各 slot 偏移量。
#### WareHouse使用 warehouse_factory
```python
from unilabos.resources.warehouse import warehouse_factory
from unilabos.registry.decorators import resource
@resource(id="my_warehouse_4x4", category=["warehouse"], description="4x4 堆栈仓库")
def my_warehouse_4x4(name: str) -> "WareHouse":
return warehouse_factory(
name=name,
num_items_x=4, num_items_y=4, num_items_z=1,
dx=10.0, dy=10.0, dz=10.0, # 第一个 slot 的起始偏移
item_dx=147.0, item_dy=106.0, item_dz=130.0, # slot 间距
resource_size_x=127.0, resource_size_y=85.0, resource_size_z=100.0, # slot 尺寸
model="my_warehouse_4x4",
col_offset=0, # 列标签起始偏移0 → A01, 4 → A05
layout="row-major", # "row-major" 行优先 / "col-major" 列优先 / "vertical-col-major" 竖向
)
```
`warehouse_factory` 参数说明:
- `dx/dy/dz`:第一个 slot 相对 WareHouse 原点的偏移mm
- `item_dx/item_dy/item_dz`:相邻 slot 间距mm需根据实际物理间距测量
- `resource_size_x/y/z`:每个 slot 的可放置区域尺寸
- `layout`:影响 slot 标签和坐标映射
- `"row-major"`A01,A02,...,B01,B02,...(行优先,适合横向排列)
- `"col-major"`A01,B01,...,A02,B02,...(列优先)
- `"vertical-col-major"`竖向排列y 坐标反向
#### Deck 组装 WareHouse
Deck 通过 `setup()` 将多个 WareHouse 放置到指定坐标:
```python
from pylabrobot.resources import Deck, Coordinate
from unilabos.registry.decorators import resource
@resource(id="MyStation_Deck", category=["deck"], description="我的工作站 Deck")
class MyStation_Deck(Deck):
def __init__(self, name="MyStation_Deck", size_x=2700.0, size_y=1080.0, size_z=1500.0,
category="deck", setup=False, **kwargs) -> None:
super().__init__(name=name, size_x=size_x, size_y=size_y, size_z=size_z)
if setup:
self.setup()
def setup(self) -> None:
self.warehouses = {
"堆栈1左": my_warehouse_4x4("堆栈1左"),
"堆栈1右": my_warehouse_4x4("堆栈1右"),
}
self.warehouse_locations = {
"堆栈1左": Coordinate(-200.0, 400.0, 0.0), # 自行测量计算
"堆栈1右": Coordinate(2350.0, 400.0, 0.0),
}
for wh_name, wh in self.warehouses.items():
self.assign_child_resource(wh, location=self.warehouse_locations[wh_name])
```
#### Site 模式(前端定向放置)
适用于有固定孔位/槽位的设备(如移液站 PRCXI 9300Deck 通过 `sites` 列表定义前端展示的放置位,前端据此渲染可拖拽的孔位布局:
```python
import collections
from typing import Any, Dict, List, Optional
from pylabrobot.resources import Deck, Resource, Coordinate
from unilabos.registry.decorators import resource
@resource(id="MyLabDeck", category=["deck"], description="带 Site 定向放置的 Deck")
class MyLabDeck(Deck):
# 根据设备台面实测批量计算各 slot 坐标偏移
_DEFAULT_SITE_POSITIONS = [
(0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T1-T4
(0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T5-T8
]
_DEFAULT_SITE_SIZE = {"width": 128.0, "height": 86.0, "depth": 0}
_DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "tube_rack", "adaptor"]
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
sites: Optional[List[Dict[str, Any]]] = None, **kwargs):
super().__init__(size_x, size_y, size_z, name)
if sites is not None:
self.sites = [dict(s) for s in sites]
else:
self.sites = []
for i, (x, y, z) in enumerate(self._DEFAULT_SITE_POSITIONS):
self.sites.append({
"label": f"T{i + 1}", # 前端显示的槽位标签
"visible": True, # 是否在前端可见
"position": {"x": x, "y": y, "z": z}, # 槽位物理坐标
"size": dict(self._DEFAULT_SITE_SIZE), # 槽位尺寸
"content_type": list(self._DEFAULT_CONTENT_TYPE), # 允许放入的物料类型
})
self._ordering = collections.OrderedDict(
(site["label"], None) for site in self.sites
)
def assign_child_resource(self, resource: Resource,
location: Optional[Coordinate] = None,
reassign: bool = True,
spot: Optional[int] = None):
idx = spot
if spot is None:
for i, site in enumerate(self.sites):
if site.get("label") == resource.name:
idx = i
break
if idx is None:
for i in range(len(self.sites)):
if self._get_site_resource(i) is None:
idx = i
break
if idx is None:
raise ValueError(f"No available site for '{resource.name}'")
loc = Coordinate(**self.sites[idx]["position"])
super().assign_child_resource(resource, location=loc, reassign=reassign)
def serialize(self) -> dict:
data = super().serialize()
sites_out = []
for i, site in enumerate(self.sites):
occupied = self._get_site_resource(i)
sites_out.append({
"label": site["label"],
"visible": site.get("visible", True),
"occupied_by": occupied.name if occupied else None,
"position": site["position"],
"size": site["size"],
"content_type": site["content_type"],
})
data["sites"] = sites_out
return data
```
**Site 字段说明:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `label` | str | 槽位标签(如 `"T1"`),前端显示名称,也用于匹配 resource.name |
| `visible` | bool | 是否在前端可见 |
| `position` | dict | 物理坐标 `{x, y, z}`mm需自行测量计算偏移 |
| `size` | dict | 槽位尺寸 `{width, height, depth}`mm |
| `content_type` | list | 允许放入的物料类型,如 `["plate", "tip_rack", "tube_rack", "adaptor"]` |
**参考实现:** `unilabos/devices/liquid_handling/prcxi/prcxi.py` 中的 `PRCXI9300Deck`4x4 共 16 个 site
---
## 文件位置
```
unilabos/resources/
├── <project>/ # 按项目分组
│ ├── bottles.py # Bottle 工厂函数
│ ├── bottle_carriers.py # Carrier 工厂函数
│ ├── warehouses.py # WareHouse 工厂函数
│ └── decks.py # Deck 类定义
```
---
## 验证
```bash
# 资源可导入
python -c "from unilabos.resources.my_project.bottles import My_Reagent_Bottle; print(My_Reagent_Bottle('test'))"
# 启动测试AST 自动扫描)
unilab -g <graph>.json
```
仅在以下情况仍需 YAML第三方库资源如 pylabrobot 内置资源,无 `@resource` 装饰器)。
---
## 关键路径
| 内容 | 路径 |
|------|------|
| Bottle/Carrier 基类 | `unilabos/resources/itemized_carrier.py` |
| WareHouse 基类 + 工厂 | `unilabos/resources/warehouse.py` |
| PLR 注册 | `unilabos/resources/plr_additional_res_reg.py` |
| 装饰器定义 | `unilabos/registry/decorators.py` |

View File

@@ -1,292 +0,0 @@
# 资源高级参考
本文件是 SKILL.md 的补充,包含类继承体系、序列化/反序列化、Bioyond 物料同步、非瓶类资源和仓库工厂模式。Agent 在需要实现这些功能时按需阅读。
---
## 1. 类继承体系
```
PyLabRobot
├── Resource (PLR 基类)
│ ├── Well
│ │ └── Bottle (unilabos) → 瓶/小瓶/烧杯/反应器
│ ├── Deck
│ │ └── 自定义 Deck 类 (unilabos) → 工作站台面
│ ├── ResourceHolder → 槽位占位符
│ └── Container
│ └── Battery (unilabos) → 组装好的电池
├── ItemizedCarrier (unilabos, 继承 Resource)
│ ├── BottleCarrier (unilabos) → 瓶载架
│ └── WareHouse (unilabos) → 堆栈仓库
├── ItemizedResource (PLR)
│ └── MagazineHolder (unilabos) → 子弹夹载架
└── ResourceStack (PLR)
└── Magazine (unilabos) → 子弹夹洞位
```
### Bottle 类细节
```python
class Bottle(Well):
def __init__(self, name, diameter, height, max_volume,
size_x=0.0, size_y=0.0, size_z=0.0,
barcode=None, category="container", model=None, **kwargs):
super().__init__(
name=name,
size_x=diameter, # PLR 用 diameter 作为 size_x/size_y
size_y=diameter,
size_z=height, # PLR 用 height 作为 size_z
max_volume=max_volume,
category=category,
model=model,
bottom_type="flat",
cross_section_type="circle"
)
```
注意 `size_x = size_y = diameter``size_z = height`
### ItemizedCarrier 核心方法
| 方法 | 说明 |
|------|------|
| `__getitem__(identifier)` | 通过索引或 Excel 标识(如 `"A01"`)访问槽位 |
| `__setitem__(identifier, resource)` | 向槽位放入资源 |
| `get_child_identifier(child)` | 获取子资源的标识符 |
| `capacity` | 总槽位数 |
| `sites` | 所有槽位字典 |
---
## 2. 序列化与反序列化
### PLR ↔ UniLab 转换
| 函数 | 位置 | 方向 |
|------|------|------|
| `ResourceTreeSet.from_plr_resources(resources)` | `resource_tracker.py` | PLR → UniLab |
| `ResourceTreeSet.to_plr_resources()` | `resource_tracker.py` | UniLab → PLR |
### `from_plr_resources` 流程
```
PLR Resource
↓ build_uuid_mapping (递归生成 UUID)
↓ resource.serialize() → dict
↓ resource.serialize_all_state() → states
↓ resource_plr_inner (递归构建 ResourceDictInstance)
ResourceTreeSet
```
关键:每个 PLR 资源通过 `unilabos_uuid` 属性携带 UUID`unilabos_extra` 携带扩展数据(如 `class` 名)。
### `to_plr_resources` 流程
```
ResourceTreeSet
↓ collect_node_data (收集 UUID、状态、扩展数据)
↓ node_to_plr_dict (转为 PLR 字典格式)
↓ find_subclass(type_name, PLRResource) (查找 PLR 子类)
↓ sub_cls.deserialize(plr_dict) (反序列化)
↓ loop_set_uuid, loop_set_extra (递归设置 UUID 和扩展)
PLR Resource
```
### Bottle 序列化
```python
class Bottle(Well):
def serialize(self) -> dict:
data = super().serialize()
return {**data, "diameter": self.diameter, "height": self.height}
@classmethod
def deserialize(cls, data: dict, allow_marshal=False):
barcode_data = data.pop("barcode", None)
instance = super().deserialize(data, allow_marshal=allow_marshal)
if barcode_data and isinstance(barcode_data, str):
instance.barcode = barcode_data
return instance
```
---
## 3. Bioyond 物料同步
### 双向转换函数
| 函数 | 位置 | 方向 |
|------|------|------|
| `resource_bioyond_to_plr(materials, type_mapping, deck)` | `graphio.py` | Bioyond → PLR |
| `resource_plr_to_bioyond(resources, type_mapping, warehouse_mapping)` | `graphio.py` | PLR → Bioyond |
### `resource_bioyond_to_plr` 流程
```
Bioyond 物料列表
↓ reverse_type_mapping: {typeName → (model, UUID)}
↓ 对每个物料:
typeName → 查映射 → model (如 "BIOYOND_PolymerStation_Reactor")
initialize_resource({"name": unique_name, "class": model})
↓ 设置 unilabos_extra (material_bioyond_id, material_bioyond_name 等)
↓ 处理 detail (子物料/坐标)
↓ 按 locationName 放入 deck.warehouses 对应槽位
PLR 资源列表
```
### `resource_plr_to_bioyond` 流程
```
PLR 资源列表
↓ 遍历每个资源:
载架(capacity > 1): 生成 details 子物料 + 坐标
单瓶: 直接映射
↓ type_mapping 查找 typeId
↓ warehouse_mapping 查找位置 UUID
↓ 组装 Bioyond 格式 (name, typeName, typeId, quantity, Parameters, locations)
Bioyond 物料列表
```
### BioyondResourceSynchronizer
工作站通过 `ResourceSynchronizer` 自动同步物料:
```python
class BioyondResourceSynchronizer(ResourceSynchronizer):
def sync_from_external(self) -> bool:
all_data = []
all_data.extend(api_client.stock_material('{"typeMode": 0}')) # 耗材
all_data.extend(api_client.stock_material('{"typeMode": 1}')) # 样品
all_data.extend(api_client.stock_material('{"typeMode": 2}')) # 试剂
unilab_resources = resource_bioyond_to_plr(
all_data,
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
deck=self.workstation.deck
)
# 更新 deck 上的资源
```
---
## 4. 非瓶类资源
### ElectrodeSheet极片
路径:`unilabos/resources/battery/electrode_sheet.py`
```python
class ElectrodeSheet(ResourcePLR):
"""片状材料(极片、隔膜、弹片、垫片等)"""
_unilabos_state = {
"diameter": 0.0,
"thickness": 0.0,
"mass": 0.0,
"material_type": "",
"color": "",
"info": "",
}
```
工厂函数:`PositiveCan`, `PositiveElectrode`, `NegativeCan`, `NegativeElectrode`, `SpringWasher`, `FlatWasher`, `AluminumFoil`
### Battery电池
```python
class Battery(Container):
"""组装好的电池"""
_unilabos_state = {
"color": "",
"electrolyte_name": "",
"open_circuit_voltage": 0.0,
}
```
### Magazine / MagazineHolder子弹夹
```python
class Magazine(ResourceStack):
"""子弹夹洞位,可堆叠 ElectrodeSheet"""
# direction, max_sheets
class MagazineHolder(ItemizedResource):
"""多洞位子弹夹"""
# hole_diameter, hole_depth, max_sheets_per_hole
```
工厂函数 `magazine_factory()``create_homogeneous_resources` 生成洞位,可选预填 `ElectrodeSheet``Battery`
---
## 5. 仓库工厂模式参考
### 实际 warehouse 工厂函数示例
```python
# 行优先 4x4 仓库
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
return warehouse_factory(
name=name,
num_items_x=4, num_items_y=4, num_items_z=1,
dx=10.0, dy=10.0, dz=10.0,
item_dx=147.0, item_dy=106.0, item_dz=130.0,
layout="row-major", # A01,A02,A03,A04, B01,...
)
# 右侧 4x4 仓库(列名偏移)
def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
return warehouse_factory(
name=name,
num_items_x=4, num_items_y=4, num_items_z=1,
dx=10.0, dy=10.0, dz=10.0,
item_dx=147.0, item_dy=106.0, item_dz=130.0,
col_offset=4, # A05,A06,A07,A08
layout="row-major",
)
# 竖向仓库(站内试剂存放)
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
return warehouse_factory(
name=name,
num_items_x=1, num_items_y=2, num_items_z=1,
dx=10.0, dy=10.0, dz=10.0,
item_dx=147.0, item_dy=106.0, item_dz=130.0,
layout="vertical-col-major",
)
# 行偏移F 行开始)
def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse:
return warehouse_factory(
name=name,
num_items_x=3, num_items_y=5, num_items_z=1,
dx=10.0, dy=10.0, dz=10.0,
item_dx=159.0, item_dy=183.0, item_dz=130.0,
row_offset=row_offset, # 0→A行起5→F行起
layout="row-major",
)
```
### layout 类型说明
| layout | 命名顺序 | 适用场景 |
|--------|---------|---------|
| `col-major` (默认) | A01,B01,C01,D01, A02,B02,... | 列优先,标准堆栈 |
| `row-major` | A01,A02,A03,A04, B01,B02,... | 行优先Bioyond 前端展示 |
| `vertical-col-major` | 竖向排列,标签从底部开始 | 竖向仓库(试剂存放、测密度) |
---
## 6. 关键路径
| 内容 | 路径 |
|------|------|
| Bottle/Carrier 基类 | `unilabos/resources/itemized_carrier.py` |
| WareHouse 类 + 工厂 | `unilabos/resources/warehouse.py` |
| ResourceTreeSet 转换 | `unilabos/resources/resource_tracker.py` |
| Bioyond 物料转换 | `unilabos/resources/graphio.py` |
| Bioyond 仓库定义 | `unilabos/resources/bioyond/warehouses.py` |
| 电池资源 | `unilabos/resources/battery/` |
| PLR 注册 | `unilabos/resources/plr_additional_res_reg.py` |

View File

@@ -1,328 +0,0 @@
---
name: create-device-skill
description: Create a skill for any Uni-Lab device by extracting action schemas from the device registry. Use when the user wants to create a new device skill, add device API documentation, or set up action schemas for a device.
---
# 创建设备 Skill 指南
本 meta-skill 教你如何为任意 Uni-Lab-OS 设备创建完整的 API 操作技能(参考 `unilab-device-api` 的成功案例)。
## 数据源
- **设备注册表**: `unilabos_data/req_device_registry_upload.json`
- **结构**: `{ "resources": [{ "id": "<device_id>", "class": { "module": "<python_module:ClassName>", "action_value_mappings": { ... } } }] }`
- **生成时机**: `unilab` 启动并完成注册表上传后自动生成
- **module 字段**: 格式 `unilabos.devices.xxx.yyy:ClassName`,可转为源码路径 `unilabos/devices/xxx/yyy.py`,阅读源码可了解参数含义和设备行为
## 创建流程
### Step 0 — 收集必备信息(缺一不可,否则询问后终止)
开始前**必须**确认以下 4 项信息全部就绪。如果用户未提供任何一项,**立即询问并终止当前流程**,等用户补齐后再继续。
向用户提问:「请提供你的 unilab 启动参数,我需要以下信息:」
#### 必备项 ①ak / sk认证凭据
来源:启动命令的 `--ak` `--sk` 参数,或 config.py 中的 `ak = "..."` `sk = "..."`
获取后立即生成 AUTH token
```bash
python ./scripts/gen_auth.py <ak> <sk>
# 或从 config.py 提取
python ./scripts/gen_auth.py --config <config.py>
```
认证算法:`base64(ak:sk)``Authorization: Lab <token>`
#### 必备项 ②:--addr目标环境
决定 API 请求发往哪个服务器。从启动命令的 `--addr` 参数获取:
| `--addr` 值 | BASE URL |
|-------------|----------|
| `test` | `https://uni-lab.test.bohrium.com` |
| `uat` | `https://uni-lab.uat.bohrium.com` |
| `local` | `http://127.0.0.1:48197` |
| 不传(默认) | `https://uni-lab.bohrium.com` |
| 其他自定义 URL | 直接使用该 URL |
#### 必备项 ③req_device_registry_upload.json设备注册表
数据文件由 `unilab` 启动时自动生成,需要定位它:
**推断 working_dir**(即 `unilabos_data` 所在目录):
| 条件 | working_dir 取值 |
|------|------------------|
| 传了 `--working_dir` | `<working_dir>/unilabos_data/`(若子目录已存在则直接用) |
| 仅传了 `--config` | `<config 文件所在目录>/unilabos_data/` |
| 都没传 | `<当前工作目录>/unilabos_data/` |
**按优先级搜索文件**
```
<推断的 working_dir>/unilabos_data/req_device_registry_upload.json
<推断的 working_dir>/req_device_registry_upload.json
<workspace 根目录>/unilabos_data/req_device_registry_upload.json
```
也可以直接 Glob 搜索:`**/req_device_registry_upload.json`
找到后**必须检查文件修改时间**并告知用户:「找到注册表文件 `<路径>`,生成于 `<时间>`。请确认这是最近一次启动生成的。」超过 1 天提醒用户是否需要重新启动 `unilab`
**如果文件不存在** → 告知用户先运行 `unilab` 启动命令,等日志出现 `注册表响应数据已保存` 后再执行本流程。**终止。**
#### 必备项 ④:目标设备
用户需要明确要为哪个设备创建 skill。可以是设备名称如「PRCXI 移液站」)或 device_id`liquid_handler.prcxi`)。
如果用户不确定,运行提取脚本列出所有设备供选择:
```bash
python ./scripts/extract_device_actions.py --registry <找到的文件路径>
```
#### 完整示例
用户提供:
```
--ak a1fd9d4e-xxxx-xxxx-xxxx-d9a69c09f0fd
--sk 136ff5c6-xxxx-xxxx-xxxx-a03e301f827b
--addr test
--port 8003
--disable_browser
```
从中提取:
- ✅ ak/sk → 运行 `gen_auth.py` 得到 `AUTH="Authorization: Lab YTFmZDlk..."`
- ✅ addr=test → `BASE=https://uni-lab.test.bohrium.com`
- ✅ 搜索 `unilabos_data/req_device_registry_upload.json` → 找到并确认时间
- ✅ 用户指明目标设备 → 如 `liquid_handler.prcxi`
**四项全部就绪后才进入 Step 1。**
### Step 1 — 列出可用设备
运行提取脚本,列出所有设备及 action 数量和 Python 源码路径,让用户选择:
```bash
# 自动搜索(默认在 unilabos_data/ 和当前目录查找)
python ./scripts/extract_device_actions.py
# 指定注册表文件路径
python ./scripts/extract_device_actions.py --registry <path/to/req_device_registry_upload.json>
```
脚本输出包含每个设备的 **Python 源码路径**(从 `class.module` 转换),可用于后续阅读源码理解参数含义。
### Step 2 — 提取 Action Schema
用户选择设备后,运行提取脚本:
```bash
python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./skills/<skill-name>/actions/
```
脚本会显示设备的 Python 源码路径和类名,方便阅读源码了解参数含义。
每个 action 生成一个 JSON 文件,包含:
- `type` — 作为 API 调用的 `action_type`
- `schema` — 完整 JSON Schema`properties.goal.properties` 参数定义)
- `goal` — goal 字段映射(含占位符 `$placeholder`
- `goal_default` — 默认值
### Step 3 — 写 action-index.md
按模板为每个 action 写条目:
```markdown
### `<action_name>`
<用途描述(一句话)>
- **Schema**: [`actions/<filename>.json`](actions/<filename>.json)
- **核心参数**: `param1`, `param2`(从 schema.required 获取)
- **可选参数**: `param3`, `param4`
- **占位符字段**: `field`(需填入物料信息,值以 `$` 开头)
```
描述规则:
-`schema.properties` 读参数列表schema 已提升为 goal 内容)
-`schema.required` 区分核心/可选参数
- 按功能分类(移液、枪头、外设等)
- 标注 `placeholder_keys` 中的字段类型:
- `unilabos_resources`**ResourceSlot**,填入 `{id, name, uuid}`id 是路径格式,从资源树取物料节点)
- `unilabos_devices`**DeviceSlot**,填入路径字符串如 `"/host_node"`(从资源树筛选 type=device
- `unilabos_nodes`**NodeSlot**,填入路径字符串如 `"/PRCXI/PRCXI_Deck"`(资源树中任意节点)
- `unilabos_class`**ClassSlot**,填入类名字符串如 `"container"`(从注册表查找)
- array 类型字段 → `[{id, name, uuid}, ...]`
- 特殊:`create_resource``res_id`ResourceSlot可填不存在的路径
### Step 4 — 写 SKILL.md
直接复用 `unilab-device-api` 的 API 模板10 个 endpoint修改
- 设备名称
- Action 数量
- 目录列表
- Session state 中的 `device_name`
- **AUTH 头** — 使用 Step 0 中 `gen_auth.py` 生成的 `Authorization: Lab <token>`(不要硬编码 `Api` 类型的 key
- **Python 源码路径** — 在 SKILL.md 开头注明设备对应的源码文件,方便参考参数含义
- **Slot 字段表** — 列出本设备哪些 action 的哪些字段需要填入 Slot物料/设备/节点/类名)
API 模板结构:
```markdown
## 设备信息
- device_id, Python 源码路径, 设备类名
## 前置条件(缺一不可)
- ak/sk → AUTH, --addr → BASE URL
## Session State
- lab_uuid通过 API #1 自动匹配,不要问用户), device_name
## API Endpoints (10 个)
# 注意:
# - #1 获取 lab 列表 + 自动匹配 lab_uuid遍历 is_admin 的 lab
# 调用 /lab/info/{uuid} 比对 access_key == ak
# - #2 创建工作流用 POST /lab/workflow
# - #10 获取资源树路径含 lab_uuid: /lab/material/download/{lab_uuid}
## Placeholder Slot 填写规则
- unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"}
- unilabos_devices → DeviceSlot → "/parent/device" 路径字符串
- unilabos_nodes → NodeSlot → "/parent/node" 路径字符串
- unilabos_class → ClassSlot → "class_name" 字符串
- 特例create_resource 的 res_id 允许填不存在的路径
- 列出本设备所有 Slot 字段、类型及含义
## 渐进加载策略
## 完整工作流 Checklist
```
### Step 5 — 验证
检查文件完整性:
- [ ] `SKILL.md` 包含 10 个 API endpoint
- [ ] `SKILL.md` 包含 Placeholder Slot 填写规则ResourceSlot / DeviceSlot / NodeSlot / ClassSlot + create_resource 特例)和本设备的 Slot 字段表
- [ ] `action-index.md` 列出所有 action 并有描述
- [ ] `actions/` 目录中每个 action 有对应 JSON 文件
- [ ] JSON 文件包含 `type`, `schema`(已提升为 goal 内容), `goal`, `goal_default`, `placeholder_keys` 字段
- [ ] 描述能让 agent 判断该用哪个 action
## Action JSON 文件结构
```json
{
"type": "LiquidHandlerTransfer", // → API 的 action_type
"goal": { // goal 字段映射
"sources": "sources",
"targets": "targets",
"tip_racks": "tip_racks",
"asp_vols": "asp_vols"
},
"schema": { // ← 直接是 goal 的 schema已提升
"type": "object",
"properties": { // 参数定义(即请求中 goal 的字段)
"sources": { "type": "array", "items": { "type": "object" } },
"targets": { "type": "array", "items": { "type": "object" } },
"asp_vols": { "type": "array", "items": { "type": "number" } }
},
"required": [...],
"_unilabos_placeholder_info": { // ← Slot 类型标记
"sources": "unilabos_resources",
"targets": "unilabos_resources",
"tip_racks": "unilabos_resources"
}
},
"goal_default": { ... }, // 默认值
"placeholder_keys": { // ← 汇总所有 Slot 字段
"sources": "unilabos_resources", // ResourceSlot
"targets": "unilabos_resources",
"tip_racks": "unilabos_resources",
"target_device_id": "unilabos_devices" // DeviceSlot
}
}
```
> **注意**`schema` 已由脚本从原始 `schema.properties.goal` 提升为顶层,直接包含参数定义。
> `schema.properties` 中的字段即为 API 请求 `param.goal` 中的字段。
## Placeholder Slot 类型体系
`placeholder_keys` / `_unilabos_placeholder_info` 中有 4 种值,对应不同的填写方式:
| placeholder 值 | Slot 类型 | 填写格式 | 选取范围 |
|---------------|-----------|---------|---------|
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅**物料**节点(不含设备) |
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅**设备**节点type=device路径字符串 |
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | **设备 + 物料**,即所有节点,路径字符串 |
| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已上报的资源类 name |
### ResourceSlot`unilabos_resources`
最常见的类型。从资源树中选取**物料**节点(孔板、枪头盒、试剂槽等):
```json
{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-2cb8-419d-8db5-d3ba056fb3c2"}
```
- 单个schema type=object`{"id": "/path/name", "name": "name", "uuid": "xxx"}`
- 数组schema type=array`[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...]`
- `id` 本身是从 parent 计算的路径格式
- 根据 action 语义选择正确的物料(如 `sources` = 液体来源,`targets` = 目标位置)
> **特例**`create_resource` 的 `res_id` 字段,目标物料可能**尚不存在**,此时直接填写期望的路径(如 `"/workstation/container1"`),不需要 uuid。
### DeviceSlot`unilabos_devices`
填写**设备路径字符串**。从资源树中筛选 type=device 的节点,从 parent 计算路径:
```
"/host_node"
"/bioyond_cell/reaction_station"
```
- 只填路径字符串,不需要 `{id, uuid}` 对象
- 根据 action 语义选择正确的设备(如 `target_device_id` = 目标设备)
### NodeSlot`unilabos_nodes`
范围 = 设备 + 物料。即资源树中**所有节点**都可以选,填写**路径字符串**
```
"/PRCXI/PRCXI_Deck"
```
- 使用场景:当参数既可能指向物料也可能指向设备时(如 `PumpTransferProtocol``from_vessel`/`to_vessel``create_resource``parent`
### ClassSlot`unilabos_class`
填写注册表中已上报的**资源类 name**。从本地 `req_resource_registry_upload.json` 中查找:
```
"container"
```
### 通过 API #10 获取资源树
```bash
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
```
注意 `lab_uuid` 在路径中(不是查询参数)。资源树返回所有节点,每个节点包含 `id`(路径格式)、`name``uuid``type``parent` 等字段。填写 Slot 时需根据 placeholder 类型筛选正确的节点。
## 最终目录结构
```
./<skill-name>/
├── SKILL.md # API 端点 + 渐进加载指引
├── action-index.md # 动作索引:描述/用途/核心参数
└── actions/ # 每个 action 的完整 JSON Schema
├── action1.json
├── action2.json
└── ...
```

View File

@@ -1,200 +0,0 @@
#!/usr/bin/env python3
"""
从 req_device_registry_upload.json 中提取指定设备的 action schema。
用法:
# 列出所有设备及 action 数量(自动搜索注册表文件)
python extract_device_actions.py
# 指定注册表文件路径
python extract_device_actions.py --registry <path/to/req_device_registry_upload.json>
# 提取指定设备的 action 到目录
python extract_device_actions.py <device_id> <output_dir>
python extract_device_actions.py --registry <path> <device_id> <output_dir>
示例:
python extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json
python extract_device_actions.py liquid_handler.prcxi .cursor/skills/unilab-device-api/actions/
"""
import json
import os
import sys
from datetime import datetime
REGISTRY_FILENAME = "req_device_registry_upload.json"
def find_registry(explicit_path=None):
"""
查找 req_device_registry_upload.json 文件。
搜索优先级:
1. 用户通过 --registry 显式指定的路径
2. <cwd>/unilabos_data/req_device_registry_upload.json
3. <cwd>/req_device_registry_upload.json
4. <script所在目录>/../../.. (workspace根) 下的 unilabos_data/
5. 向上逐级搜索父目录(最多 5 层)
"""
if explicit_path:
if os.path.isfile(explicit_path):
return explicit_path
if os.path.isdir(explicit_path):
fp = os.path.join(explicit_path, REGISTRY_FILENAME)
if os.path.isfile(fp):
return fp
print(f"警告: 指定的路径不存在: {explicit_path}")
return None
candidates = [
os.path.join("unilabos_data", REGISTRY_FILENAME),
REGISTRY_FILENAME,
]
for c in candidates:
if os.path.isfile(c):
return c
script_dir = os.path.dirname(os.path.abspath(__file__))
workspace_root = os.path.normpath(os.path.join(script_dir, "..", "..", ".."))
for c in candidates:
path = os.path.join(workspace_root, c)
if os.path.isfile(path):
return path
cwd = os.getcwd()
for _ in range(5):
parent = os.path.dirname(cwd)
if parent == cwd:
break
cwd = parent
for c in candidates:
path = os.path.join(cwd, c)
if os.path.isfile(path):
return path
return None
def load_registry(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def list_devices(data):
"""列出所有包含 action_value_mappings 的设备,同时返回 module 路径"""
resources = data.get('resources', [])
devices = []
for res in resources:
rid = res.get('id', '')
cls = res.get('class', {})
avm = cls.get('action_value_mappings', {})
module = cls.get('module', '')
if avm:
devices.append((rid, len(avm), module))
return devices
def flatten_schema_to_goal(action_data):
"""将 schema 中嵌套的 goal 内容提升为顶层 schema去掉 feedback/result 包装"""
schema = action_data.get('schema', {})
goal_schema = schema.get('properties', {}).get('goal', {})
if goal_schema:
action_data = dict(action_data)
action_data['schema'] = goal_schema
return action_data
def extract_actions(data, device_id, output_dir):
"""提取指定设备的 action schema 到独立 JSON 文件"""
resources = data.get('resources', [])
for res in resources:
if res.get('id') == device_id:
cls = res.get('class', {})
module = cls.get('module', '')
avm = cls.get('action_value_mappings', {})
if not avm:
print(f"设备 {device_id} 没有 action_value_mappings")
return []
if module:
py_path = module.split(":")[0].replace(".", "/") + ".py"
class_name = module.split(":")[-1] if ":" in module else ""
print(f"Python 源码: {py_path}")
if class_name:
print(f"设备类: {class_name}")
os.makedirs(output_dir, exist_ok=True)
written = []
for action_name in sorted(avm.keys()):
action_data = flatten_schema_to_goal(avm[action_name])
filename = action_name.replace('-', '_') + '.json'
filepath = os.path.join(output_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(action_data, f, indent=2, ensure_ascii=False)
written.append(filename)
print(f" {filepath}")
return written
print(f"设备 {device_id} 未找到")
return []
def main():
args = sys.argv[1:]
explicit_registry = None
if "--registry" in args:
idx = args.index("--registry")
if idx + 1 < len(args):
explicit_registry = args[idx + 1]
args = args[:idx] + args[idx + 2:]
else:
print("错误: --registry 需要指定路径")
sys.exit(1)
registry_path = find_registry(explicit_registry)
if not registry_path:
print(f"错误: 找不到 {REGISTRY_FILENAME}")
print()
print("解决方法:")
print(" 1. 先运行 unilab 启动命令,等待注册表生成")
print(" 2. 用 --registry 指定文件路径:")
print(f" python {sys.argv[0]} --registry <path/to/{REGISTRY_FILENAME}>")
print()
print("搜索过的路径:")
for p in [
os.path.join("unilabos_data", REGISTRY_FILENAME),
REGISTRY_FILENAME,
os.path.join("<workspace_root>", "unilabos_data", REGISTRY_FILENAME),
]:
print(f" - {p}")
sys.exit(1)
print(f"注册表: {registry_path}")
mtime = os.path.getmtime(registry_path)
gen_time = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
size_mb = os.path.getsize(registry_path) / (1024 * 1024)
print(f"生成时间: {gen_time} (文件大小: {size_mb:.1f} MB)")
data = load_registry(registry_path)
if len(args) == 0:
devices = list_devices(data)
print(f"\n找到 {len(devices)} 个设备:")
print(f"{'设备 ID':<50} {'Actions':>7} {'Python 模块'}")
print("-" * 120)
for did, count, module in sorted(devices, key=lambda x: x[0]):
py_path = module.split(":")[0].replace(".", "/") + ".py" if module else ""
print(f"{did:<50} {count:>7} {py_path}")
elif len(args) == 2:
device_id = args[0]
output_dir = args[1]
print(f"\n提取 {device_id} 的 actions 到 {output_dir}/")
written = extract_actions(data, device_id, output_dir)
if written:
print(f"\n共写入 {len(written)} 个 action 文件")
else:
print("用法:")
print(" python extract_device_actions.py [--registry <path>] # 列出设备")
print(" python extract_device_actions.py [--registry <path>] <device_id> <dir> # 提取 actions")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -1,69 +0,0 @@
#!/usr/bin/env python3
"""
从 ak/sk 生成 UniLab API Authorization header。
算法: base64(ak:sk) → "Authorization: Lab <token>"
用法:
python gen_auth.py <ak> <sk>
python gen_auth.py --config <config.py>
示例:
python gen_auth.py myak mysk
python gen_auth.py --config experiments/config.py
"""
import base64
import re
import sys
def gen_auth(ak: str, sk: str) -> str:
token = base64.b64encode(f"{ak}:{sk}".encode("utf-8")).decode("utf-8")
return token
def extract_from_config(config_path: str) -> tuple:
"""从 config.py 中提取 ak 和 sk"""
with open(config_path, "r", encoding="utf-8") as f:
content = f.read()
ak_match = re.search(r'''ak\s*=\s*["']([^"']+)["']''', content)
sk_match = re.search(r'''sk\s*=\s*["']([^"']+)["']''', content)
if not ak_match or not sk_match:
return None, None
return ak_match.group(1), sk_match.group(1)
def main():
args = sys.argv[1:]
if len(args) == 2 and args[0] == "--config":
ak, sk = extract_from_config(args[1])
if not ak or not sk:
print(f"错误: 在 {args[1]} 中未找到 ak/sk 配置")
print("期望格式: ak = \"xxx\" sk = \"xxx\"")
sys.exit(1)
print(f"配置文件: {args[1]}")
elif len(args) == 2:
ak, sk = args
else:
print("用法:")
print(" python gen_auth.py <ak> <sk>")
print(" python gen_auth.py --config <config.py>")
sys.exit(1)
token = gen_auth(ak, sk)
print(f"ak: {ak}")
print(f"sk: {sk}")
print()
print(f"Authorization header:")
print(f" Authorization: Lab {token}")
print()
print(f"curl 用法:")
print(f' curl -H "Authorization: Lab {token}" ...')
print()
print(f"Shell 变量:")
print(f' AUTH="Authorization: Lab {token}"')
if __name__ == "__main__":
main()

View File

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

1
.gitignore vendored
View File

@@ -5,7 +5,6 @@ output/
unilabos_data/
pyrightconfig.json
.cursorignore
device_package*/
## Python
# Byte-compiled / optimized / DLL files

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -439,9 +439,6 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
1. 访问 Web 界面,进入"仪器耗材"模块
2. 在"仪器设备"区域找到并添加上述设备
3. 在"物料耗材"区域找到并添加容器
4. 在workstation中配置protocol_type包含PumpTransferProtocol
![添加Protocol类型](image/add_protocol.png)
![物料列表](image/material.png)
@@ -452,9 +449,8 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
**操作步骤:**
1. 将两个 `container` 拖拽到 `workstation`
2.`virtual_multiway_valve` 拖拽到 `workstation`
3. `virtual_transfer_pump` 拖拽到 `workstation`
4. 在画布上连接它们(建立父子关系)
2.`virtual_transfer_pump` 拖拽到 `workstation`
3. 在画布上连接它们(建立父子关系)
![设备连接](image/links.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 275 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,213 +0,0 @@
{
"workflow": [
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines",
"targets": "Liquid_1",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines",
"targets": "Liquid_2",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines",
"targets": "Liquid_3",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines_2",
"targets": "Liquid_4",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines_2",
"targets": "Liquid_5",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines_2",
"targets": "Liquid_6",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines_3",
"targets": "dest_set",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines_3",
"targets": "dest_set_2",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "cell_lines_3",
"targets": "dest_set_3",
"asp_vol": 100.0,
"dis_vol": 74.75,
"asp_flow_rate": 94.0,
"dis_flow_rate": 95.5
}
}
],
"reagent": {
"Liquid_1": {
"slot": 1,
"well": [
"A4",
"A7",
"A10"
],
"labware": "rep 1"
},
"Liquid_4": {
"slot": 1,
"well": [
"A4",
"A7",
"A10"
],
"labware": "rep 1"
},
"dest_set": {
"slot": 1,
"well": [
"A4",
"A7",
"A10"
],
"labware": "rep 1"
},
"Liquid_2": {
"slot": 2,
"well": [
"A3",
"A5",
"A8"
],
"labware": "rep 2"
},
"Liquid_5": {
"slot": 2,
"well": [
"A3",
"A5",
"A8"
],
"labware": "rep 2"
},
"dest_set_2": {
"slot": 2,
"well": [
"A3",
"A5",
"A8"
],
"labware": "rep 2"
},
"Liquid_3": {
"slot": 3,
"well": [
"A4",
"A6",
"A10"
],
"labware": "rep 3"
},
"Liquid_6": {
"slot": 3,
"well": [
"A4",
"A6",
"A10"
],
"labware": "rep 3"
},
"dest_set_3": {
"slot": 3,
"well": [
"A4",
"A6",
"A10"
],
"labware": "rep 3"
},
"cell_lines": {
"slot": 4,
"well": [
"A1",
"A3",
"A5"
],
"labware": "DRUG + YOYO-MEDIA"
},
"cell_lines_2": {
"slot": 4,
"well": [
"A1",
"A3",
"A5"
],
"labware": "DRUG + YOYO-MEDIA"
},
"cell_lines_3": {
"slot": 4,
"well": [
"A1",
"A3",
"A5"
],
"labware": "DRUG + YOYO-MEDIA"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -327,7 +327,6 @@ def job_add(req: JobAddReq) -> JobData:
queue_item,
action_type=action_type,
action_kwargs=action_args,
sample_material=req.sample_material,
server_info=server_info,
)

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,31 @@ xrd_d7mate:
title: connect参数
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-start_from_string:
feedback: {}
goal: {}
@@ -60,14 +85,11 @@ xrd_d7mate:
goal:
properties:
params:
anyOf:
- type: string
- type: object
type: string
required:
- params
type: object
result:
type: object
result: {}
required:
- goal
title: start_from_string参数
@@ -83,18 +105,21 @@ xrd_d7mate:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -105,38 +130,38 @@ xrd_d7mate:
get_sample_down:
feedback: {}
goal:
int_input: int_input
sample_station: sample_station
sample_station: 1
goal_default:
int_input: 0
handles: {}
placeholder_keys: {}
result:
return_info: return_info
success: success
result: {}
schema:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: IntSingleInput_Feedback
type: object
goal:
additionalProperties: false
properties:
int_input:
maximum: 2147483647
minimum: -2147483648
type: integer
required:
- int_input
title: IntSingleInput_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: IntSingleInput_Result
type: object
required:
@@ -154,18 +179,21 @@ xrd_d7mate:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -183,18 +211,21 @@ xrd_d7mate:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -207,25 +238,26 @@ xrd_d7mate:
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result:
return_info: return_info
result: {}
schema:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -242,35 +274,42 @@ xrd_d7mate:
sample_id: ''
start_theta: 10.0
goal_default:
end_theta: null
exp_time: null
increment: null
sample_id: null
start_theta: null
end_theta: 80.0
exp_time: 0.5
increment: 0.02
sample_id: Sample001
start_theta: 10.0
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 送样完成后,发送样品信息和采集参数
properties:
feedback:
properties: {}
required: []
title: SampleReadyInput_Feedback
type: object
goal:
properties:
end_theta:
description: 结束角度≥5.5°且必须大于start_theta
minimum: 5.5
type: number
exp_time:
description: 曝光时间0.1-5.0秒)
maximum: 5.0
minimum: 0.1
type: number
increment:
description: 角度增量≥0.005
minimum: 0.005
type: number
sample_id:
description: 样品标识符
type: string
start_theta:
description: 起始角度≥5°
minimum: 5.0
type: number
required:
- sample_id
@@ -281,11 +320,19 @@ xrd_d7mate:
title: SampleReadyInput_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SampleReadyInput_Result
type: object
required:
- goal
title: send_sample_ready参数
title: SampleReadyInput
type: object
type: UniLabJsonCommand
set_power_off:
@@ -293,25 +340,26 @@ xrd_d7mate:
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result:
return_info: return_info
result: {}
schema:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -324,25 +372,26 @@ xrd_d7mate:
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result:
return_info: return_info
result: {}
schema:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -356,16 +405,18 @@ xrd_d7mate:
current: 30.0
voltage: 40.0
goal_default:
current: null
voltage: null
current: 30.0
voltage: 40.0
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 设置高压电源电压和电流
properties:
feedback:
properties: {}
required: []
title: VoltageCurrentInput_Feedback
type: object
goal:
properties:
current:
@@ -380,11 +431,19 @@ xrd_d7mate:
title: VoltageCurrentInput_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: VoltageCurrentInput_Result
type: object
required:
- goal
title: set_voltage_current参数
title: VoltageCurrentInput
type: object
type: UniLabJsonCommand
start:
@@ -394,12 +453,11 @@ xrd_d7mate:
end_theta: 80.0
exp_time: 0.1
increment: 0.05
sample_id: ''
sample_id: 样品名称
start_theta: 10.0
string: ''
wait_minutes: 3.0
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。
@@ -408,42 +466,54 @@ xrd_d7mate:
goal:
properties:
end_theta:
default: 80.0
description: 结束角度≥5.5°且必须大于start_theta
type: number
minimum: 5.5
type: string
exp_time:
default: 0.1
description: 曝光时间0.1-5.0秒)
type: number
maximum: 5.0
minimum: 0.1
type: string
increment:
default: 0.05
description: 角度增量≥0.005
type: number
minimum: 0.005
type: string
sample_id:
default: ''
description: 样品标识符
type: string
start_theta:
default: 10.0
description: 起始角度≥5°
type: number
minimum: 5.0
type: string
string:
default: ''
description: 字符串格式的参数输入,如果提供则优先解析使用
type: string
wait_minutes:
default: 3.0
description: 允许上样后等待分钟数
minimum: 0.0
type: number
required: []
required:
- sample_id
- start_theta
- end_theta
- increment
- exp_time
title: StartWorkflow_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: StartWorkflow_Result
type: object
required:
- goal
title: start参数
title: StartWorkflow
type: object
type: UniLabJsonCommand
start_auto_mode:
@@ -451,15 +521,17 @@ xrd_d7mate:
goal:
status: true
goal_default:
status: null
status: true
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 启动或停止自动模式
properties:
feedback:
properties: {}
required: []
title: BoolSingleInput_Feedback
type: object
goal:
properties:
status:
@@ -470,16 +542,25 @@ xrd_d7mate:
title: BoolSingleInput_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: BoolSingleInput_Result
type: object
required:
- goal
title: start_auto_mode参数
title: BoolSingleInput
type: object
type: UniLabJsonCommand
module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient
status_types:
current_acquire_data: dict
sample_down: dict
sample_request: dict
sample_status: dict
type: python
@@ -505,13 +586,16 @@ xrd_d7mate:
properties:
current_acquire_data:
type: object
sample_down:
type: object
sample_request:
type: object
sample_status:
type: object
required:
- current_acquire_data
- sample_request
- current_acquire_data
- sample_status
- sample_down
type: object
version: 1.0.0

View File

@@ -8,25 +8,26 @@ zhida_gcms:
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result:
return_info: return_info
result: {}
schema:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -76,6 +77,31 @@ zhida_gcms:
title: connect参数
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
get_methods:
feedback: {}
goal: {}
@@ -86,18 +112,21 @@ zhida_gcms:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -115,18 +144,21 @@ zhida_gcms:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -144,18 +176,21 @@ zhida_gcms:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -168,25 +203,26 @@ zhida_gcms:
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result:
return_info: return_info
result: {}
schema:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
additionalProperties: true
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
required:
- return_info
title: EmptyIn_Result
type: object
required:
@@ -198,35 +234,35 @@ zhida_gcms:
feedback: {}
goal:
string: string
text: text
goal_default:
string: ''
handles: {}
placeholder_keys: {}
result:
return_info: return_info
success: success
result: {}
schema:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: StrSingleInput_Feedback
type: object
goal:
additionalProperties: false
properties:
string:
type: string
required:
- string
title: StrSingleInput_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: StrSingleInput_Result
type: object
required:
@@ -237,36 +273,36 @@ zhida_gcms:
start_with_csv_file:
feedback: {}
goal:
csv_file_path: csv_file_path
string: string
goal_default:
string: ''
handles: {}
placeholder_keys: {}
result:
return_info: return_info
success: success
result: {}
schema:
description: ''
properties:
feedback:
additionalProperties: true
properties: {}
required: []
title: StrSingleInput_Feedback
type: object
goal:
additionalProperties: false
properties:
string:
type: string
required:
- string
title: StrSingleInput_Goal
type: object
result:
additionalProperties: false
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: StrSingleInput_Result
type: object
required:
@@ -307,8 +343,8 @@ zhida_gcms:
version:
type: object
required:
- methods
- status
- methods
- version
type: object
version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ YB_20ml_fenyeping:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_5ml_fenyeping:
category:
@@ -21,6 +22,7 @@ YB_5ml_fenyeping:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_jia_yang_tou_da:
category:
@@ -33,6 +35,7 @@ YB_jia_yang_tou_da:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_pei_ye_da_Bottle:
category:
@@ -45,6 +48,7 @@ YB_pei_ye_da_Bottle:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_pei_ye_xiao_Bottle:
category:
@@ -57,6 +61,7 @@ YB_pei_ye_xiao_Bottle:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_qiang_tou:
category:
@@ -69,6 +74,7 @@ YB_qiang_tou:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye_Bottle:
category:
@@ -82,4 +88,5 @@ YB_ye_Bottle:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -9,6 +9,7 @@ YB_100ml_yeti:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_20ml_fenyepingban:
category:
@@ -21,6 +22,7 @@ YB_20ml_fenyepingban:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_5ml_fenyepingban:
category:
@@ -33,6 +35,7 @@ YB_5ml_fenyepingban:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6StockCarrier:
category:
@@ -45,6 +48,7 @@ YB_6StockCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6VialCarrier:
category:
@@ -57,6 +61,7 @@ YB_6VialCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_gao_nian_ye_Bottle:
category:
@@ -69,6 +74,7 @@ YB_gao_nian_ye_Bottle:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_gaonianye:
category:
@@ -81,6 +87,7 @@ YB_gaonianye:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_jia_yang_tou_da_Carrier:
category:
@@ -93,6 +100,7 @@ YB_jia_yang_tou_da_Carrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_peiyepingdaban:
category:
@@ -105,6 +113,7 @@ YB_peiyepingdaban:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_peiyepingxiaoban:
category:
@@ -117,6 +126,7 @@ YB_peiyepingxiaoban:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_qiang_tou_he:
category:
@@ -129,6 +139,7 @@ YB_qiang_tou_he:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_shi_pei_qi_kuai:
category:
@@ -141,6 +152,7 @@ YB_shi_pei_qi_kuai:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye:
category:
@@ -153,6 +165,7 @@ YB_ye:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye_100ml_Bottle:
category:
@@ -165,4 +178,5 @@ YB_ye_100ml_Bottle:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -8,6 +8,7 @@ BIOYOND_PolymerStation_1BottleCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_1FlaskCarrier:
category:
@@ -19,6 +20,7 @@ BIOYOND_PolymerStation_1FlaskCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_6StockCarrier:
category:
@@ -30,6 +32,7 @@ BIOYOND_PolymerStation_6StockCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_8StockCarrier:
category:
@@ -41,4 +44,5 @@ BIOYOND_PolymerStation_8StockCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -8,6 +8,7 @@ BIOYOND_PolymerPreparationStation_Deck:
handles: []
icon: 配液站.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerReactionStation_Deck:
category:
@@ -19,6 +20,7 @@ BIOYOND_PolymerReactionStation_Deck:
handles: []
icon: 反应站.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_YB_Deck:
category:
@@ -30,6 +32,7 @@ BIOYOND_YB_Deck:
handles: []
icon: 配液站.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0
CoincellDeck:
category:
@@ -41,4 +44,5 @@ CoincellDeck:
handles: []
icon: koudian.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,3 +1,24 @@
disposal:
category:
- disposal
- waste
- resource_container
class:
module: unilabos.resources.disposal:Disposal
type: unilabos
description: 废料处理位置,用于处理实验废料
handles:
- data_key: disposal_access
data_source: handle
data_type: fluid
handler_key: access
io_type: target
label: access
side: NORTH
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
hplc_plate:
category:
- resource_container
@@ -19,6 +40,56 @@ hplc_plate:
- 3.1416
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro
type: resource
registry_type: resource
version: 1.0.0
maintenance:
category:
- maintenance
- position
- resource_container
class:
module: unilabos.resources.maintenance:Maintenance
type: unilabos
description: 维护位置,用于设备维护和校准
handles:
- data_key: maintenance_access
data_source: handle
data_type: mechanical
handler_key: access
io_type: target
label: access
side: NORTH
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
plate:
category:
- plate
- labware
- resource_container
class:
module: unilabos.resources.plate:Plate
type: unilabos
description: 实验板,用于放置样品和试剂
handles:
- data_key: plate_access
data_source: handle
data_type: mechanical
handler_key: access
io_type: target
label: access
side: NORTH
- data_key: sample_wells
data_source: handle
data_type: fluid
handler_key: wells
io_type: target
label: wells
side: CENTER
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
plate_96:
category:
@@ -41,6 +112,7 @@ plate_96:
- 0
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro
type: resource
registry_type: resource
version: 1.0.0
plate_96_high:
category:
@@ -63,6 +135,35 @@ plate_96_high:
- 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro
type: resource
registry_type: resource
version: 1.0.0
tip_rack:
category:
- tip_rack
- labware
- resource_container
class:
module: unilabos.resources.tip_rack:TipRack
type: unilabos
description: 枪头架资源,用于存放和管理移液器枪头
handles:
- data_key: tip_access
data_source: handle
data_type: mechanical
handler_key: access
io_type: target
label: access
side: NORTH
- data_key: tip_pickup
data_source: handle
data_type: mechanical
handler_key: pickup
io_type: target
label: pickup
side: SOUTH
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
tiprack_96_high:
category:
@@ -94,6 +195,7 @@ tiprack_96_high:
- 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro
type: resource
registry_type: resource
version: 1.0.0
tiprack_box:
category:
@@ -125,4 +227,5 @@ tiprack_box:
- 0
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro
type: resource
registry_type: resource
version: 1.0.0

View File

@@ -29,6 +29,7 @@ bottle_container:
- 0
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro
type: resource
registry_type: resource
version: 1.0.0
tube_container:
category:
@@ -61,4 +62,5 @@ tube_container:
- 0
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro
type: resource
registry_type: resource
version: 1.0.0

View File

@@ -12,4 +12,5 @@ TransformXYZDeck:
mesh: liquid_transform_xyz
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro
type: device
registry_type: resource
version: 1.0.0

View File

@@ -12,6 +12,7 @@ OTDeck:
mesh: opentrons_liquid_handler
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro
type: device
registry_type: resource
version: 1.0.0
hplc_station:
category:
@@ -27,4 +28,5 @@ hplc_station:
mesh: hplc_station
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro
type: device
registry_type: resource
version: 1.0.0

View File

@@ -8,4 +8,5 @@ Opentrons_96_adapter_Vb:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -8,6 +8,7 @@ appliedbiosystemsmicroamp_384_wellplate_40ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
biorad_384_wellplate_50ul:
category:
@@ -19,6 +20,7 @@ biorad_384_wellplate_50ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
biorad_96_wellplate_200ul_pcr:
category:
@@ -30,6 +32,7 @@ biorad_96_wellplate_200ul_pcr:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
corning_12_wellplate_6point9ml_flat:
category:
@@ -41,6 +44,7 @@ corning_12_wellplate_6point9ml_flat:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
corning_24_wellplate_3point4ml_flat:
category:
@@ -52,6 +56,7 @@ corning_24_wellplate_3point4ml_flat:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
corning_384_wellplate_112ul_flat:
category:
@@ -63,6 +68,7 @@ corning_384_wellplate_112ul_flat:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
corning_48_wellplate_1point6ml_flat:
category:
@@ -74,6 +80,7 @@ corning_48_wellplate_1point6ml_flat:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
corning_6_wellplate_16point8ml_flat:
category:
@@ -85,6 +92,7 @@ corning_6_wellplate_16point8ml_flat:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
corning_96_wellplate_360ul_flat:
category:
@@ -96,6 +104,7 @@ corning_96_wellplate_360ul_flat:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
nest_96_wellplate_100ul_pcr_full_skirt:
category:
@@ -127,6 +136,7 @@ nest_96_wellplate_100ul_pcr_full_skirt:
- 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
type: resource
registry_type: resource
version: 1.0.0
nest_96_wellplate_200ul_flat:
category:
@@ -138,6 +148,7 @@ nest_96_wellplate_200ul_flat:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
nest_96_wellplate_2ml_deep:
category:
@@ -160,6 +171,7 @@ nest_96_wellplate_2ml_deep:
- 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
type: resource
registry_type: resource
version: 1.0.0
thermoscientificnunc_96_wellplate_1300ul:
category:
@@ -171,6 +183,7 @@ thermoscientificnunc_96_wellplate_1300ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
thermoscientificnunc_96_wellplate_2000ul:
category:
@@ -182,6 +195,7 @@ thermoscientificnunc_96_wellplate_2000ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
usascientific_96_wellplate_2point4ml_deep:
category:
@@ -193,4 +207,5 @@ usascientific_96_wellplate_2point4ml_deep:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -8,6 +8,7 @@ agilent_1_reservoir_290ml:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
axygen_1_reservoir_90ml:
category:
@@ -19,6 +20,7 @@ axygen_1_reservoir_90ml:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
nest_12_reservoir_15ml:
category:
@@ -30,6 +32,7 @@ nest_12_reservoir_15ml:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
nest_1_reservoir_195ml:
category:
@@ -41,6 +44,7 @@ nest_1_reservoir_195ml:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
nest_1_reservoir_290ml:
category:
@@ -52,6 +56,7 @@ nest_1_reservoir_290ml:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
usascientific_12_reservoir_22ml:
category:
@@ -63,4 +68,5 @@ usascientific_12_reservoir_22ml:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -8,6 +8,7 @@ eppendorf_96_tiprack_1000ul_eptips:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
eppendorf_96_tiprack_10ul_eptips:
category:
@@ -19,6 +20,7 @@ eppendorf_96_tiprack_10ul_eptips:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
geb_96_tiprack_1000ul:
category:
@@ -30,6 +32,7 @@ geb_96_tiprack_1000ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
geb_96_tiprack_10ul:
category:
@@ -41,6 +44,7 @@ geb_96_tiprack_10ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_96_filtertiprack_1000ul:
category:
@@ -71,6 +75,7 @@ opentrons_96_filtertiprack_1000ul:
- 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
type: resource
registry_type: resource
version: 1.0.0
opentrons_96_filtertiprack_10ul:
category:
@@ -82,6 +87,7 @@ opentrons_96_filtertiprack_10ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_96_filtertiprack_200ul:
category:
@@ -93,6 +99,7 @@ opentrons_96_filtertiprack_200ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_96_filtertiprack_20ul:
category:
@@ -104,6 +111,7 @@ opentrons_96_filtertiprack_20ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_96_tiprack_1000ul:
category:
@@ -115,6 +123,7 @@ opentrons_96_tiprack_1000ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_96_tiprack_10ul:
category:
@@ -126,6 +135,7 @@ opentrons_96_tiprack_10ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_96_tiprack_20ul:
category:
@@ -137,6 +147,7 @@ opentrons_96_tiprack_20ul:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_96_tiprack_300ul:
category:

View File

@@ -8,6 +8,7 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic:
category:
@@ -19,6 +20,7 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_10_tuberack_nest_4x50ml_6x15ml_conical:
category:
@@ -30,6 +32,7 @@ opentrons_10_tuberack_nest_4x50ml_6x15ml_conical:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_15_tuberack_falcon_15ml_conical:
category:
@@ -41,6 +44,7 @@ opentrons_15_tuberack_falcon_15ml_conical:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_15_tuberack_nest_15ml_conical:
category:
@@ -52,6 +56,7 @@ opentrons_15_tuberack_nest_15ml_conical:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_aluminumblock_generic_2ml_screwcap:
category:
@@ -63,6 +68,7 @@ opentrons_24_aluminumblock_generic_2ml_screwcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_aluminumblock_nest_1point5ml_snapcap:
category:
@@ -74,6 +80,7 @@ opentrons_24_aluminumblock_nest_1point5ml_snapcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap:
category:
@@ -85,6 +92,7 @@ opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap:
category:
@@ -96,6 +104,7 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic:
category:
@@ -107,6 +116,7 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic:
category:
@@ -118,6 +128,7 @@ opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_generic_2ml_screwcap:
category:
@@ -129,6 +140,7 @@ opentrons_24_tuberack_generic_2ml_screwcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_nest_0point5ml_screwcap:
category:
@@ -140,6 +152,7 @@ opentrons_24_tuberack_nest_0point5ml_screwcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_nest_1point5ml_screwcap:
category:
@@ -151,6 +164,7 @@ opentrons_24_tuberack_nest_1point5ml_screwcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_nest_1point5ml_snapcap:
category:
@@ -162,6 +176,7 @@ opentrons_24_tuberack_nest_1point5ml_snapcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_nest_2ml_screwcap:
category:
@@ -173,6 +188,7 @@ opentrons_24_tuberack_nest_2ml_screwcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_24_tuberack_nest_2ml_snapcap:
category:
@@ -184,6 +200,7 @@ opentrons_24_tuberack_nest_2ml_snapcap:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_6_tuberack_falcon_50ml_conical:
category:
@@ -195,6 +212,7 @@ opentrons_6_tuberack_falcon_50ml_conical:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_6_tuberack_nest_50ml_conical:
category:
@@ -206,6 +224,7 @@ opentrons_6_tuberack_nest_50ml_conical:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
opentrons_96_well_aluminum_block:
category:
@@ -217,4 +236,5 @@ opentrons_96_well_aluminum_block:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -29,4 +29,5 @@ container:
side: WEST
icon: Flask.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -8,6 +8,7 @@ POST_PROCESS_Raw_1BottleCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
POST_PROCESS_Reaction_1BottleCarrier:
category:
@@ -19,4 +20,5 @@ POST_PROCESS_Reaction_1BottleCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -9,4 +9,5 @@ post_process_deck:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -9,6 +9,7 @@ PRCXI_30mm_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Adapter:
category:
@@ -21,6 +22,7 @@ PRCXI_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Deep10_Adapter:
category:
@@ -33,6 +35,7 @@ PRCXI_Deep10_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Deep300_Adapter:
category:
@@ -45,6 +48,7 @@ PRCXI_Deep300_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_PCR_Adapter:
category:
@@ -57,6 +61,7 @@ PRCXI_PCR_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Reservoir_Adapter:
category:
@@ -69,6 +74,7 @@ PRCXI_Reservoir_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Tip10_Adapter:
category:
@@ -81,6 +87,7 @@ PRCXI_Tip10_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Tip1250_Adapter:
category:
@@ -93,6 +100,7 @@ PRCXI_Tip1250_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Tip300_Adapter:
category:
@@ -105,4 +113,5 @@ PRCXI_Tip300_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -9,6 +9,7 @@ PRCXI_48_DeepWell:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_96_DeepWell:
category:
@@ -21,6 +22,7 @@ PRCXI_96_DeepWell:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_AGenBio_4_troughplate:
category:
@@ -33,6 +35,7 @@ PRCXI_AGenBio_4_troughplate:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_BioER_96_wellplate:
category:
@@ -45,6 +48,7 @@ PRCXI_BioER_96_wellplate:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_BioRad_384_wellplate:
category:
@@ -57,6 +61,7 @@ PRCXI_BioRad_384_wellplate:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_CellTreat_96_wellplate:
category:
@@ -69,6 +74,7 @@ PRCXI_CellTreat_96_wellplate:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_PCR_Plate_200uL_nonskirted:
category:
@@ -81,6 +87,7 @@ PRCXI_PCR_Plate_200uL_nonskirted:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_PCR_Plate_200uL_semiskirted:
category:
@@ -93,6 +100,7 @@ PRCXI_PCR_Plate_200uL_semiskirted:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_PCR_Plate_200uL_skirted:
category:
@@ -105,6 +113,7 @@ PRCXI_PCR_Plate_200uL_skirted:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_nest_12_troughplate:
category:
@@ -117,6 +126,7 @@ PRCXI_nest_12_troughplate:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_nest_1_troughplate:
category:
@@ -129,4 +139,5 @@ PRCXI_nest_1_troughplate:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -9,6 +9,7 @@ PRCXI_1000uL_Tips:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_10uL_Tips:
category:
@@ -21,6 +22,7 @@ PRCXI_10uL_Tips:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_10ul_eTips:
category:
@@ -33,6 +35,7 @@ PRCXI_10ul_eTips:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_1250uL_Tips:
category:
@@ -45,6 +48,7 @@ PRCXI_1250uL_Tips:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_200uL_Tips:
category:
@@ -57,6 +61,7 @@ PRCXI_200uL_Tips:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_300ul_Tips:
category:
@@ -69,4 +74,5 @@ PRCXI_300ul_Tips:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -9,4 +9,5 @@ PRCXI_trash:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -9,4 +9,5 @@ PRCXI_EP_Adapter:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,724 +0,0 @@
"""
注册表工具函数
从 registry.py 中提取的纯工具函数,包括:
- docstring 解析
- 类型字符串 → JSON Schema 转换
- AST 类型节点解析
- TypedDict / Slot / Handle 等辅助检测
"""
import inspect
import logging
import re
import typing
from typing import Any, Dict, List, Optional, Tuple, Union
from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance
from unilabos.utils.cls_creator import import_class
_logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# 异常
# ---------------------------------------------------------------------------
class ROSMsgNotFound(Exception):
pass
# ---------------------------------------------------------------------------
# Docstring 解析 (Google-style)
# ---------------------------------------------------------------------------
_SECTION_RE = re.compile(r"^(\w[\w\s]*):\s*$")
def parse_docstring(docstring: Optional[str]) -> Dict[str, Any]:
"""
解析 Google-style docstring提取描述和参数说明。
Returns:
{"description": "短描述", "params": {"param1": "参数1描述", ...}}
"""
result: Dict[str, Any] = {"description": "", "params": {}}
if not docstring:
return result
lines = docstring.strip().splitlines()
if not lines:
return result
result["description"] = lines[0].strip()
in_args = False
current_param: Optional[str] = None
current_desc_parts: list = []
for line in lines[1:]:
stripped = line.strip()
section_match = _SECTION_RE.match(stripped)
if section_match:
if current_param is not None:
result["params"][current_param] = "\n".join(current_desc_parts).strip()
current_param = None
current_desc_parts = []
section_name = section_match.group(1).lower()
in_args = section_name in ("args", "arguments", "parameters", "params")
continue
if not in_args:
continue
if ":" in stripped and not stripped.startswith(" "):
if current_param is not None:
result["params"][current_param] = "\n".join(current_desc_parts).strip()
param_part, _, desc_part = stripped.partition(":")
param_name = param_part.strip().split("(")[0].strip()
current_param = param_name
current_desc_parts = [desc_part.strip()]
elif current_param is not None:
aline = line
if aline.startswith(" "):
aline = aline[4:]
elif aline.startswith("\t"):
aline = aline[1:]
current_desc_parts.append(aline.strip())
if current_param is not None:
result["params"][current_param] = "\n".join(current_desc_parts).strip()
return result
# ---------------------------------------------------------------------------
# 类型常量
# ---------------------------------------------------------------------------
SIMPLE_TYPE_MAP = {
"str": "string",
"string": "string",
"int": "integer",
"integer": "integer",
"float": "number",
"number": "number",
"bool": "boolean",
"boolean": "boolean",
"list": "array",
"array": "array",
"dict": "object",
"object": "object",
}
ARRAY_TYPES = {"list", "List", "tuple", "Tuple", "set", "Set", "Sequence", "Iterable"}
OBJECT_TYPES = {"dict", "Dict", "Mapping"}
WRAPPER_TYPES = {"Optional"}
SLOT_TYPES = {"ResourceSlot", "DeviceSlot"}
# ---------------------------------------------------------------------------
# 简单类型映射
# ---------------------------------------------------------------------------
def get_json_schema_type(type_str: str) -> str:
"""简单类型名 -> JSON Schema type"""
return SIMPLE_TYPE_MAP.get(type_str.lower(), "string")
# ---------------------------------------------------------------------------
# AST 类型解析
# ---------------------------------------------------------------------------
def parse_type_node(type_str: str):
"""将类型注解字符串解析为 AST 节点,失败返回 None。"""
import ast as _ast
try:
return _ast.parse(type_str.strip(), mode="eval").body
except Exception:
return None
def _collect_bitor(node, out: list):
"""递归收集 X | Y | Z 的所有分支。"""
import ast as _ast
if isinstance(node, _ast.BinOp) and isinstance(node.op, _ast.BitOr):
_collect_bitor(node.left, out)
_collect_bitor(node.right, out)
else:
out.append(node)
def type_node_to_schema(
node,
import_map: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""将 AST 类型注解节点递归转换为 JSON Schema dict。
当提供 import_map 时,对于未知类名会尝试通过 import_map 解析模块路径,
然后 import 真实类型对象来生成 schema (支持 TypedDict 等)。
映射规则:
- Optional[X] → X 的 schema (剥掉 Optional)
- Union[X, Y] → {"anyOf": [X_schema, Y_schema]}
- List[X] / Tuple[X] / Set[X] → {"type": "array", "items": X_schema}
- Dict[K, V] → {"type": "object", "additionalProperties": V_schema}
- Literal["a", "b"] → {"type": "string", "enum": ["a", "b"]}
- TypedDict (via import_map) → {"type": "object", "properties": {...}}
- 基本类型 str/int/... → {"type": "string"/"integer"/...}
"""
import ast as _ast
# --- Name 节点: str / int / dict / ResourceSlot / 自定义类 ---
if isinstance(node, _ast.Name):
name = node.id
if name in SLOT_TYPES:
return {"$slot": name}
json_type = SIMPLE_TYPE_MAP.get(name.lower())
if json_type:
return {"type": json_type}
# 尝试通过 import_map 解析并 import 真实类型
if import_map and name in import_map:
type_obj = resolve_type_object(import_map[name])
if type_obj is not None:
return type_to_schema(type_obj)
# 未知类名 → 无法转 schema 的自定义类型默认当 object
return {"type": "object"}
if isinstance(node, _ast.Constant):
if isinstance(node.value, str):
return {"type": SIMPLE_TYPE_MAP.get(node.value.lower(), "string")}
return {"type": "string"}
# --- Subscript 节点: List[X], Dict[K,V], Optional[X], Literal[...] 等 ---
if isinstance(node, _ast.Subscript):
base_name = node.value.id if isinstance(node.value, _ast.Name) else ""
# Optional[X] → 剥掉
if base_name in WRAPPER_TYPES:
return type_node_to_schema(node.slice, import_map)
# Union[X, None] → 剥掉 None; Union[X, Y] → anyOf
if base_name == "Union":
elts = node.slice.elts if isinstance(node.slice, _ast.Tuple) else [node.slice]
non_none = [
e
for e in elts
if not (isinstance(e, _ast.Constant) and e.value is None)
and not (isinstance(e, _ast.Name) and e.id == "None")
]
if len(non_none) == 1:
return type_node_to_schema(non_none[0], import_map)
if len(non_none) > 1:
return {"anyOf": [type_node_to_schema(e, import_map) for e in non_none]}
return {"type": "string"}
# Literal["a", "b", 1] → enum
if base_name == "Literal":
elts = node.slice.elts if isinstance(node.slice, _ast.Tuple) else [node.slice]
values = []
for e in elts:
if isinstance(e, _ast.Constant):
values.append(e.value)
elif isinstance(e, _ast.Name):
values.append(e.id)
if values:
return {"type": "string", "enum": values}
return {"type": "string"}
# List / Tuple / Set → array
if base_name in ARRAY_TYPES:
if isinstance(node.slice, _ast.Tuple) and node.slice.elts:
inner_node = node.slice.elts[0]
else:
inner_node = node.slice
return {"type": "array", "items": type_node_to_schema(inner_node, import_map)}
# Dict → object
if base_name in OBJECT_TYPES:
schema: Dict[str, Any] = {"type": "object"}
if isinstance(node.slice, _ast.Tuple) and len(node.slice.elts) >= 2:
val_node = node.slice.elts[1]
# Dict[str, Any] → 不加 additionalProperties (Any 等同于无约束)
is_any = (isinstance(val_node, _ast.Name) and val_node.id == "Any") or (
isinstance(val_node, _ast.Constant) and val_node.value is None
)
if not is_any:
val_schema = type_node_to_schema(val_node, import_map)
schema["additionalProperties"] = val_schema
return schema
# --- BinOp: X | Y (Python 3.10+) → 当 Union 处理 ---
if isinstance(node, _ast.BinOp) and isinstance(node.op, _ast.BitOr):
parts: list = []
_collect_bitor(node, parts)
non_none = [
p
for p in parts
if not (isinstance(p, _ast.Constant) and p.value is None)
and not (isinstance(p, _ast.Name) and p.id == "None")
]
if len(non_none) == 1:
return type_node_to_schema(non_none[0], import_map)
if len(non_none) > 1:
return {"anyOf": [type_node_to_schema(p, import_map) for p in non_none]}
return {"type": "string"}
return {"type": "string"}
# ---------------------------------------------------------------------------
# 真实类型对象解析 (import-based)
# ---------------------------------------------------------------------------
def resolve_type_object(type_ref: str) -> Optional[Any]:
"""通过 'module.path:ClassName' 格式的引用 import 并返回真实类型对象。
对于 typing 内置名 (str, int, List 等) 直接返回 None (由 AST 路径处理)。
import 失败时静默返回 None。
"""
if ":" not in type_ref:
return None
try:
return import_class(type_ref)
except Exception:
return None
def is_typed_dict_class(obj: Any) -> bool:
"""检查对象是否是 TypedDict 类。"""
if obj is None:
return False
try:
from typing_extensions import is_typeddict
return is_typeddict(obj)
except ImportError:
if isinstance(obj, type):
return hasattr(obj, "__required_keys__") and hasattr(obj, "__optional_keys__")
return False
def type_to_schema(tp: Any) -> Dict[str, Any]:
"""将真实 typing 对象递归转换为 JSON Schema dict。
支持:
- 基本类型: str, int, float, bool → {"type": "string"/"integer"/...}
- typing 泛型: List[X], Dict[K,V], Optional[X], Union[X,Y], Literal[...]
- TypedDict → {"type": "object", "properties": {...}, "required": [...]}
- 自定义类 (ResourceSlot 等) → {"$slot": "..."} 或 {"type": "string"}
"""
origin = getattr(tp, "__origin__", None)
args = getattr(tp, "__args__", None)
# --- None / NoneType ---
if tp is type(None):
return {"type": "null"}
# --- 基本类型 ---
if tp is str:
return {"type": "string"}
if tp is int:
return {"type": "integer"}
if tp is float:
return {"type": "number"}
if tp is bool:
return {"type": "boolean"}
# --- TypedDict ---
if is_typed_dict_class(tp):
try:
return TypedDictMessageInstance.get_json_schema_from_typed_dict(tp)
except Exception:
return {"type": "object"}
# --- Literal ---
if origin is typing.Literal:
values = list(args) if args else []
return {"type": "string", "enum": values}
# --- Optional / Union ---
if origin is typing.Union:
non_none = [a for a in (args or ()) if a is not type(None)]
if len(non_none) == 1:
return type_to_schema(non_none[0])
if len(non_none) > 1:
return {"anyOf": [type_to_schema(a) for a in non_none]}
return {"type": "string"}
# --- List / Sequence / Set / Tuple / Iterable ---
if origin in (list, tuple, set, frozenset) or (
origin is not None
and getattr(origin, "__name__", "") in ("Sequence", "Iterable", "Iterator", "MutableSequence")
):
if args:
return {"type": "array", "items": type_to_schema(args[0])}
return {"type": "array"}
# --- Dict / Mapping ---
if origin in (dict,) or (origin is not None and getattr(origin, "__name__", "") in ("Mapping", "MutableMapping")):
schema: Dict[str, Any] = {"type": "object"}
if args and len(args) >= 2:
schema["additionalProperties"] = type_to_schema(args[1])
return schema
# --- Slot 类型 ---
if isinstance(tp, type):
name = tp.__name__
if name in SLOT_TYPES:
return {"$slot": name}
# --- 其他未知类型 fallback ---
if isinstance(tp, type):
return {"type": "object"}
return {"type": "string"}
# ---------------------------------------------------------------------------
# Slot / Placeholder 检测
# ---------------------------------------------------------------------------
def detect_slot_type(ptype) -> Tuple[Optional[str], bool]:
"""检测参数类型是否为 ResourceSlot / DeviceSlot。
兼容多种格式:
- runtime: "unilabos.registry.placeholder_type:ResourceSlot"
- runtime tuple: ("list", "unilabos.registry.placeholder_type:ResourceSlot")
- AST 裸名: "ResourceSlot", "List[ResourceSlot]", "Optional[ResourceSlot]"
Returns: (slot_name | None, is_list)
"""
ptype_str = str(ptype)
# 快速路径: 字符串里根本没有 Slot
if "ResourceSlot" not in ptype_str and "DeviceSlot" not in ptype_str:
return (None, False)
# runtime 格式: 完整模块路径
if isinstance(ptype, str):
if ptype.endswith(":ResourceSlot") or ptype == "ResourceSlot":
return ("ResourceSlot", False)
if ptype.endswith(":DeviceSlot") or ptype == "DeviceSlot":
return ("DeviceSlot", False)
# AST 复杂格式: List[ResourceSlot], Optional[ResourceSlot] 等
if "[" in ptype:
node = parse_type_node(ptype)
if node is not None:
schema = type_node_to_schema(node)
# 直接是 slot
if "$slot" in schema:
return (schema["$slot"], False)
# array 包裹 slot: {"type": "array", "items": {"$slot": "..."}}
items = schema.get("items", {})
if isinstance(items, dict) and "$slot" in items:
return (items["$slot"], True)
return (None, False)
# runtime tuple 格式
if isinstance(ptype, tuple) and len(ptype) == 2:
inner_str = str(ptype[1])
if "ResourceSlot" in inner_str:
return ("ResourceSlot", True)
if "DeviceSlot" in inner_str:
return ("DeviceSlot", True)
return (None, False)
def detect_placeholder_keys(params: list) -> Dict[str, str]:
"""Detect parameters that reference ResourceSlot or DeviceSlot."""
result: Dict[str, str] = {}
for p in params:
ptype = p.get("type", "")
if "ResourceSlot" in str(ptype):
result[p["name"]] = "unilabos_resources"
elif "DeviceSlot" in str(ptype):
result[p["name"]] = "unilabos_devices"
return result
# ---------------------------------------------------------------------------
# Handle 规范化
# ---------------------------------------------------------------------------
def normalize_ast_handles(handles_raw: Any) -> List[Dict[str, Any]]:
"""Convert AST-parsed handle structures to the standard registry format."""
if not handles_raw:
return []
# handle_type → io_type 映射 (AST 内部类名 → YAML 标准字段值)
_HANDLE_TYPE_TO_IO_TYPE = {
"input": "target",
"output": "source",
"action_input": "action_target",
"action_output": "action_source",
}
result: List[Dict[str, Any]] = []
for h in handles_raw:
if isinstance(h, dict):
call = h.get("_call", "")
if "InputHandle" in call:
handle_type = "input"
elif "OutputHandle" in call:
handle_type = "output"
elif "ActionInputHandle" in call:
handle_type = "action_input"
elif "ActionOutputHandle" in call:
handle_type = "action_output"
else:
handle_type = h.get("handle_type", "unknown")
io_type = _HANDLE_TYPE_TO_IO_TYPE.get(handle_type, handle_type)
entry: Dict[str, Any] = {
"handler_key": h.get("key", ""),
"data_type": h.get("data_type", ""),
"io_type": io_type,
}
side = h.get("side")
if side:
if isinstance(side, str) and "." in side:
val = side.rsplit(".", 1)[-1]
side = val.lower() if val in ("LEFT", "RIGHT", "TOP", "BOTTOM") else val
entry["side"] = side
label = h.get("label")
if label:
entry["label"] = label
data_key = h.get("data_key")
if data_key:
entry["data_key"] = data_key
data_source = h.get("data_source")
if data_source:
if isinstance(data_source, str) and "." in data_source:
val = data_source.rsplit(".", 1)[-1]
data_source = val.lower() if val in ("HANDLE", "EXECUTOR") else val
entry["data_source"] = data_source
description = h.get("description")
if description:
entry["description"] = description
result.append(entry)
return result
def normalize_ast_action_handles(handles_raw: Any) -> Dict[str, Any]:
"""Convert AST-parsed action handle list to {"input": [...], "output": [...]}.
Mirrors the runtime behavior of decorators._action_handles_to_dict:
- ActionInputHandle => grouped under "input"
- ActionOutputHandle => grouped under "output"
Field mapping: key -> handler_key (matches Pydantic serialization_alias).
"""
if not handles_raw or not isinstance(handles_raw, list):
return {}
input_list: List[Dict[str, Any]] = []
output_list: List[Dict[str, Any]] = []
for h in handles_raw:
if not isinstance(h, dict):
continue
call = h.get("_call", "")
is_input = "ActionInputHandle" in call or "InputHandle" in call
is_output = "ActionOutputHandle" in call or "OutputHandle" in call
entry: Dict[str, Any] = {
"handler_key": h.get("key", ""),
"data_type": h.get("data_type", ""),
"label": h.get("label", ""),
}
for opt_key in ("side", "data_key", "data_source", "description", "io_type"):
val = h.get(opt_key)
if val is not None:
# Only resolve enum-style refs (e.g. DataSource.HANDLE -> handle) for data_source/side
# data_key values like "wells.@flatten", "@this.0@@@plate" must be preserved as-is
if (
isinstance(val, str)
and "." in val
and opt_key not in ("io_type", "data_key")
):
val = val.rsplit(".", 1)[-1].lower()
entry[opt_key] = val
# io_type: only add when explicitly set; do not default output to "sink" (YAML convention omits it)
if "io_type" not in entry and is_input:
entry["io_type"] = "source"
if is_input:
input_list.append(entry)
elif is_output:
output_list.append(entry)
result: Dict[str, Any] = {}
if input_list:
result["input"] = input_list
# Always include output (empty list when no outputs) to match YAML
result["output"] = output_list
return result
# ---------------------------------------------------------------------------
# Schema 辅助
# ---------------------------------------------------------------------------
def wrap_action_schema(
goal_schema: Dict[str, Any],
action_name: str,
description: str = "",
result_schema: Optional[Dict[str, Any]] = None,
feedback_schema: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
将 goal 参数 schema 包装为标准的 action schema 格式:
{ "properties": { "goal": ..., "feedback": ..., "result": ... }, ... }
"""
# 去掉 auto- 前缀用于 title/description与 YAML 路径保持一致
display_name = action_name.removeprefix("auto-")
return {
"title": f"{display_name}参数",
"description": description or f"{display_name}的参数schema",
"type": "object",
"properties": {
"goal": goal_schema,
"feedback": feedback_schema or {},
"result": result_schema or {},
},
"required": ["goal"],
}
def preserve_field_descriptions(new_schema: Dict[str, Any], prev_schema: Dict[str, Any]):
"""递归保留之前 schema 中各字段的 description / title。
覆盖顶层以及嵌套 properties如 goal.properties.xxx.description
"""
if not prev_schema or not new_schema:
return
prev_props = prev_schema.get("properties", {})
new_props = new_schema.get("properties", {})
for field_name, prev_field in prev_props.items():
if field_name not in new_props:
continue
new_field = new_props[field_name]
if not isinstance(prev_field, dict) or not isinstance(new_field, dict):
continue
if "title" in prev_field:
new_field.setdefault("title", prev_field["title"])
if "description" in prev_field:
new_field.setdefault("description", prev_field["description"])
if "properties" in prev_field and "properties" in new_field:
preserve_field_descriptions(new_field, prev_field)
def strip_ros_descriptions(schema: Any):
"""递归清除 ROS schema 中自动生成的无意义 description含 rosidl_parser 内存地址)。"""
if isinstance(schema, dict):
desc = schema.get("description", "")
if isinstance(desc, str) and "rosidl_parser" in desc:
del schema["description"]
for v in schema.values():
strip_ros_descriptions(v)
elif isinstance(schema, list):
for item in schema:
strip_ros_descriptions(item)
# ---------------------------------------------------------------------------
# 深度对比
# ---------------------------------------------------------------------------
def _short(val, limit=120):
"""截断过长的值用于日志显示。"""
s = repr(val)
return s if len(s) <= limit else s[:limit] + "..."
def deep_diff(old, new, path="", max_depth=10) -> list:
"""递归对比两个对象,返回所有差异的描述列表。"""
diffs = []
if max_depth <= 0:
if old != new:
diffs.append(f"{path}: (达到最大深度) OLD≠NEW")
return diffs
if type(old) != type(new):
diffs.append(f"{path}: 类型不同 OLD={type(old).__name__}({_short(old)}) NEW={type(new).__name__}({_short(new)})")
return diffs
if isinstance(old, dict):
old_keys = set(old.keys())
new_keys = set(new.keys())
for k in sorted(new_keys - old_keys):
diffs.append(f"{path}.{k}: 新增字段 (AST有, YAML无) = {_short(new[k])}")
for k in sorted(old_keys - new_keys):
diffs.append(f"{path}.{k}: 缺失字段 (YAML有, AST无) = {_short(old[k])}")
for k in sorted(old_keys & new_keys):
diffs.extend(deep_diff(old[k], new[k], f"{path}.{k}", max_depth - 1))
elif isinstance(old, (list, tuple)):
if len(old) != len(new):
diffs.append(f"{path}: 列表长度不同 OLD={len(old)} NEW={len(new)}")
for i in range(min(len(old), len(new))):
diffs.extend(deep_diff(old[i], new[i], f"{path}[{i}]", max_depth - 1))
if len(new) > len(old):
for i in range(len(old), len(new)):
diffs.append(f"{path}[{i}]: 新增元素 = {_short(new[i])}")
elif len(old) > len(new):
for i in range(len(new), len(old)):
diffs.append(f"{path}[{i}]: 缺失元素 = {_short(old[i])}")
else:
if old != new:
diffs.append(f"{path}: OLD={_short(old)} NEW={_short(new)}")
return diffs
# ---------------------------------------------------------------------------
# MRO 方法参数解析
# ---------------------------------------------------------------------------
def resolve_method_params_via_import(module_str: str, method_name: str) -> Dict[str, str]:
"""当 AST 方法参数为空 (如 *args, **kwargs) 时, import class 并通过 MRO 获取真实方法参数.
返回 identity mapping {param_name: param_name}.
"""
if not module_str or ":" not in module_str:
return {}
try:
cls = import_class(module_str)
except Exception as e:
_logger.debug(f"[AST] resolve_method_params_via_import: import_class('{module_str}') failed: {e}")
return {}
try:
for base_cls in cls.__mro__:
if method_name not in base_cls.__dict__:
continue
method = base_cls.__dict__[method_name]
actual = getattr(method, "__wrapped__", method)
if isinstance(actual, (staticmethod, classmethod)):
actual = actual.__func__
if not callable(actual):
continue
sig = inspect.signature(actual, follow_wrapped=True)
params = [
p.name for p in sig.parameters.values()
if p.name not in ("self", "cls")
and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
]
if params:
return {p: p for p in params}
except Exception as e:
_logger.debug(f"[AST] resolve_method_params_via_import: MRO walk for '{method_name}' failed: {e}")
return {}

View File

@@ -1,6 +1,10 @@
import json
from typing import Dict, Any
from pylabrobot.resources import Container
from unilabos_msgs.msg import Resource
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
class RegularContainer(Container):
@@ -12,14 +16,12 @@ class RegularContainer(Container):
kwargs["size_y"] = 0
if "size_z" not in kwargs:
kwargs["size_z"] = 0
if "category" not in kwargs:
kwargs["category"] = "container"
self.kwargs = kwargs
super().__init__(*args, **kwargs)
self.state = {}
super().__init__(*args, category="container", **kwargs)
def load_state(self, state: Dict[str, Any]):
super().load_state(state)
self.state = state
def get_regular_container(name="container"):
@@ -27,6 +29,7 @@ def get_regular_container(name="container"):
r.category = "container"
return r
#
# class RegularContainer(object):
# # 第一个参数必须是id传入
# # noinspection PyShadowingBuiltins
@@ -86,4 +89,4 @@ def get_regular_container(name="container"):
# return to_dict
#
# def __str__(self):
# return f"{self.id}"
# return f"{self.id}"

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