Compare commits

..

92 Commits

Author SHA1 Message Date
Roy
19ca6b5db8 v0.11.3
ci(deps): bump actions/deploy-pages from 4 to 5 (#251)

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

---
updated-dependencies:
- dependency-name: actions/deploy-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>

ci(deps): bump actions/configure-pages from 5 to 6 (#252)

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

---
updated-dependencies:
- dependency-name: actions/configure-pages
  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 4 to 5 (#260)

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

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  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>

ci(deps): bump conda-incubator/setup-miniconda from 3 to 4 (#261)

Bumps [conda-incubator/setup-miniconda](https://github.com/conda-incubator/setup-miniconda) from 3 to 4.
- [Release notes](https://github.com/conda-incubator/setup-miniconda/releases)
- [Changelog](https://github.com/conda-incubator/setup-miniconda/blob/main/CHANGELOG.md)
- [Commits](https://github.com/conda-incubator/setup-miniconda/compare/v3...v4)

---
updated-dependencies:
- dependency-name: conda-incubator/setup-miniconda
  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>

Add PLC communication guide (#264)

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

* Add PLC communication guide for AI4M

Add a comprehensive developer guide (docs/developer_guide/add_PLC.md) describing the PLC integration standard used by Uni-Lab for workstation devices, using the AI4M implementation as reference. Covers rationale for using OPC UA, the opcua_nodes_*.csv node-table format, communication base classes (BaseOpcUaClient / OpcUaClientWithSubscription), data types, and subscription/cache/reconnect behavior. Documents driver patterns for AI4MDevice, three handshake paradigms (pulse, parameter handshake, id-based), registry/graph configuration (YAML/JSON), debugging tips (KEPServerEX sim, standalone run), and a checklist for onboarding new PLC-controlled equipment.
2026-05-23 23:45:17 +08:00
Xuwznln
35de4a5fee sync recent dev changes to main
Bring over recent dev-only updates for notebook-aware job state, workflow conversion modules, and conda build environment handling as a single squashed change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 22:28:31 +08:00
Xuwznln
8ba4138a09 fix macos x64 conda artifacts
Ensure macOS x64 jobs run on an Intel runner and pass the matrix platform through to rattler-build so package metadata matches the uploaded artifact.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 21:45:32 +08:00
Xuwznln
bcb1790897 v0.11.2 2026-05-14 18:21:43 +08:00
Xuwznln
916a6dfc60 env installation fix
fix pack install 2

fix pip install & git install failed

fix pack build 1

Update SKILL.md

Update Skills

Update registry for all param desc
2026-05-14 18:21:22 +08:00
Xuwznln
f71ea2a258 Support display_name & desc in new registry system 2026-04-27 20:28:18 +08:00
Xuwznln
f6b2bfaf8e upgrade to 0.11.1 2026-04-22 19:54:59 +08:00
Xuwznln
71107e9552 use gitee to install pylabrobot
fix virtual import
2026-04-22 19:54:57 +08:00
Xuwznln
1ad4766221 fix possible conversion error 2026-04-22 19:54:55 +08:00
Xuwznln
67a74172dc v0.11.0 2026-04-16 01:35:01 +08:00
Xuwznln
ccbf5378dd update workbench example
update aksk desc

print res query logs

Fix skills exec error with action type

Update Skills

Update Skills addr

Change uni-lab. to leap-lab.
Support unit in pylabrobot

Support async func.

change to leap-lab backend. Support feedback interval. Reduce cocurrent lags.

fix create_resource_with_slot

update unilabos_formulation & batch-submit-exp

scale multi exec thread up to 48

update handle creation api

fit cocurrent gap

add running status debounce

allow non @topic_config support

update skill

add placeholder keys

always free

提交实验技能

disable samples

correct sample demo ret value

新增试剂reagent

update registry

新增manual_confirm

add workstation creation skill

add virtual_sample_demo 样品追踪测试设备

add external devices param
fix registry upload missing type

fast registry load

minor fix on skill & registry

stripe ros2 schema desc
add create-device-skill

new registry system backwards to yaml

remove not exist resource

new registry sys
exp. support with add device

correct raise create resource error

ret info fix revert

ret info fix

fix prcxi check

add create_resource schema

re signal host ready event

add websocket connection timeout and improve reconnection logic

add open_timeout parameter to websocket connection
add TimeoutError and InvalidStatus exception handling
implement exponential backoff for reconnection attempts
simplify reconnection logic flow
2026-04-16 01:33:36 +08:00
Xuwznln
c001f6a151 v0.10.19
fast registry load

minor fix on skill & registry

stripe ros2 schema desc
add create-device-skill

new registry system backwards to yaml

remove not exist resource

new registry sys
exp. support with add device

add ai conventions

correct raise create resource error

ret info fix revert

ret info fix

fix prcxi check

add create_resource schema

re signal host ready event

add websocket connection timeout and improve reconnection logic

add open_timeout parameter to websocket connection
add TimeoutError and InvalidStatus exception handling
implement exponential backoff for reconnection attempts
simplify reconnection logic flow

add gzip

change pose extra to any

add isFlapY
2026-03-22 04:25:07 +08:00
Xuwznln
145fcaae65 support container as example
add z index
2026-03-03 18:03:53 +08:00
Xuwznln
a79c0a88bf fix container volume
update materials

更新prcxi deck & 新增 unilabos_resource_slot

new workflow & prcxi slot removal

fix size change
2026-03-03 17:20:06 +08:00
Xuwznln
06b6f0d804 v0.10.18 2026-02-28 09:53:31 +08:00
Xuwznln
b551e69f64 no opcua installation on macos
fix possible crash

fix deck & host_node

set liquid with tube

add test_resource_schema

fix test resource schema

registry update & workflow update

add test mode

support description & tags upload

fix config load

fix log

add registry name & add always free

correct config organic synthesis

Adapt to new scheduler, sampels, and edge upload format (#230)

* add sample_material

* adapt to new samples sys

* fix pump transfer. fix resource update when protocol & ros callback

* Adapt to new scheduler.

Feat/samples (#229)

* add sample_material

* adapt to new samples sys

adapt to new samples sys

adapt to new edge format

workflow upload & prcxi transfer liquid

lh liquid

speed up registry load

workflow upload & set liquid fix & add set liquid with plate

fix upload workflow json
2026-02-28 09:46:46 +08:00
Xuwznln
5179a7e48e workflow upload & set liquid fix & add set liquid with plate
fix upload workflow json

save class name when deserialize & protocol execute test

Support root node change pos

add unilabos_class

gather query
2026-02-02 18:26:41 +08:00
Xuwznln
3a2d9e9603 transfer liquid handles
add msg goal

Fix OT2 & ReAdd Virtual Devices
2026-01-28 11:46:54 +08:00
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
90 changed files with 5567 additions and 9038 deletions

View File

@@ -3,7 +3,7 @@
package:
name: unilabos
version: 0.10.19
version: 0.11.3
source:
path: ../../unilabos
@@ -54,7 +54,7 @@ requirements:
- pymodbus
- matplotlib
- pylibftdi
- uni-lab::unilabos-env ==0.10.19
- uni-lab::unilabos-env ==0.11.3
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.11.3
build:
noarch: generic

View File

@@ -3,7 +3,7 @@
package:
name: unilabos-full
version: 0.10.19
version: 0.11.3
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.11.3
# Documentation tools
- sphinx
- sphinx_rtd_theme

View File

@@ -5,9 +5,98 @@ description: Guide for adding new devices to Uni-Lab-OS (接入新设备). Uses
# 添加新设备到 Uni-Lab-OS
**第一步:** 使用 Read 工具读取 `docs/ai_guides/add_device.md`,获取完整的设备接入指南
本 Skill 是自包含的设备接入指南,不依赖外部文档。迁移给别人时,只复制 `.cursor/skills/add-device/SKILL.md` 即可获得核心规则、模板、验证方式和常见错误清单
该指南包含设备类别(物模型)列表、通信协议模板、常见错误检查清单等。搜索 `unilabos/devices/` 获取已有设备的实现参考。
开始实现前,仍应搜索 `unilabos/devices/` 获取同类别已有设备的接口、参数名、状态字符串和返回值风格作为参考。
---
## 接入工作流
按下面顺序推进,并在工作中维护进度:
```text
设备接入进度:
- [ ] 1. 确定设备类别(物模型)和对外单位
- [ ] 2. 确定通信协议
- [ ] 3. 收集指令协议SDK、厂商文档、寄存器表、HTTP API、用户口述
- [ ] 4. 对齐同类设备接口(搜索 unilabos/devices/
- [ ] 5. 创建驱动 unilabos/devices/<category>/<file>.py
- [ ] 6. 验证可导入、注册表扫描、启动测试
- [ ] 7. 如需要,配置实验图文件
```
## 设备类别(物模型)
优先使用已有类别。只有确实无法归类时才使用 `custom`
| 类别 ID | 说明 | 标准属性 | 标准动作 |
|---|---|---|---|
| `temperature` | 加热、冷却、温控 | `temp`, `temp_target`, `status` | `set_temperature`, `stop` |
| `pump_and_valve` | 泵、阀门、注射器 | 见子类型表 | 见子类型表 |
| `motor` | 电机、步进马达 | `position`, `status` | `enable`, `move_position`, `move_speed`, `stop` |
| `heaterstirrer` | 加热搅拌一体机 | `temp`, `stir_speed`, `status` | `set_temperature`, `stir`, `stop` |
| `balance` | 天平、称重 | `weight`, `unit`, `status` | `tare`, `read_weight` |
| `sensor` | 传感器(液位、温度等) | `value`, `level`, `status` | `read_value`, `set_threshold` |
| `liquid_handling` | 液体处理机器人 | `status`, `deck_state` | `transfer_liquid`, `aspirate`, `dispense` |
| `robot_arm` | 机械臂 | `arm_pose`, `arm_status` | `moveit_task`, `pick_and_place` |
| `workstation` | 工作站、组合设备 | `workflow_sequence`, `material_info` | `create_order`, `scheduler_start`, `scheduler_stop` |
| `virtual` | 虚拟、模拟设备 | 按模拟的真实设备定义 | 按模拟的真实设备定义 |
| `custom` | 不属于以上类别 | 用户自定义 | 用户自定义 |
`pump_and_valve` 子类型:
| 子类型 | 最小通用属性 | 最小通用动作 | 单位约定 |
|---|---|---|---|
| 注射泵syringe pump | `status`, `valve_position`, `position` | `initialize`, `set_valve_position`, `set_position`, `pull_plunger`, `push_plunger`, `stop_operation` | 体积=mL, 速度=mL/s |
| 电磁阀solenoid valve | `status`, `valve_position` | `open`, `close`, `set_valve_position` | 无 |
| 蠕动泵peristaltic pump | `status`, `speed` | `start`, `stop`, `set_speed` | 流速=mL/min |
对外暴露的属性和动作参数必须使用用户友好的物理单位mL、ul、degC、RPM 等),硬件原始值转换放在驱动内部。
## 通信协议和指令来源
先确认通信方式,再确认具体指令协议。物模型只定义设备“应该做什么”,不会告诉你硬件“具体发什么字节/请求”。
| 协议 | 常用 config 参数 | 常用依赖 | 现有抽象 |
|---|---|---|---|
| Serial (RS232/RS485) | `port`, `baudrate`, `timeout` | `pyserial` | 直接使用 `serial.Serial` |
| Modbus RTU | `port`, `baudrate`, `slave_id` | `pymodbus` | `device_comms/modbus_plc/` |
| Modbus TCP | `host`, `port`, `slave_id` | `pymodbus` | `device_comms/modbus_plc/` |
| TCP Socket | `host`, `port`, `timeout` | stdlib | 直接使用 `socket` |
| HTTP API | `url`, `token`, `timeout` | `requests` | `device_comms/rpc.py` |
| OPC UA | `url` | `opcua` | `device_comms/opcua_client/` |
| 无通信(虚拟) | 无 | 无 | 在动作中模拟行为 |
必须从以下来源之一获得指令细节:
| 来源 | 处理方式 |
|---|---|
| 现成 SDK/驱动代码 | 读取代码,提取指令逻辑,包装进 Uni-Lab-OS 类 |
| 协议文档/手册 | 解析命令、响应、校验、寄存器、错误码 |
| 用户口述 | 按描述实现指令编解码,标出不确定点 |
| 标准协议 | 使用标准实现,例如 Modbus 寄存器表、SCPI |
| 虚拟设备 | 跳过硬件通信,在动作方法中维护模拟状态 |
## 对齐已有实现(强制)
实现前必须搜索 `unilabos/devices/` 中同类别设备:
- 参数名必须与已有设备保持一致;动作方法参数名是接口契约,不要随意改成 `volume_ml``target_temp_c` 这类新名字。
- `status` 字符串值要和同类设备一致,优先使用英文稳定值,例如 `Idle``Running``Error`
- 状态属性用 `@property` + `@topic_config()` 明确声明。
- 返回值使用结构化 dict至少包含 `success`,需要给前端展示的信息放在 `message``data``error` 等字段。
## 架构选择
| 场景 | 推荐方式 |
|---|---|
| 简单设备 | 纯 Python 类 + `@device` |
| 工作站/组合设备 | `WorkstationBase` 或项目内已有工作站模式 |
| 液体处理 | `LiquidHandlerAbstract` / PyLabRobot 相关模式 |
| Modbus 设备 | 复用 `device_comms/modbus_plc/` 或项目内 Modbus 示例 |
| OPC UA 设备 | 复用 `device_comms/opcua_client/` |
| 外部独立包 | 使用 `create-device-package` skill |
---
@@ -71,6 +160,45 @@ from unilabos.registry.decorators import action
- `_` 开头的方法 → 不扫描
- `@not_action` 标记的方法 → 排除
### 参数文档 → JSON Schema 元数据
`__init__` 和 action 方法 docstring 的 `Args:` 小节里,使用以下格式生成入参 schema 的显示信息:
```python
"""
Args:
param[显示名称]: 参数说明,会写入 JSON Schema 的 description。
"""
```
- `param[显示名称]` 的显示名称会写入 goal property 的 `title`
- `:` 后面的说明会写入 goal property 的 `description`
- 如果只写 `param: 参数说明``title` 会兜底为字段名,`description` 使用参数说明。
- 如果没有写参数文档,生成器也会兜底补齐 `title=<字段名>``description=""`,但新设备应优先写清楚显示名和说明。
### 特殊参数类型ResourceSlot / DeviceSlot
需要前端选择资源或设备时用特殊类型注解registry 会自动生成 `placeholder_keys`
```python
from typing import List
from unilabos.registry.placeholder_type import DeviceSlot, ResourceSlot
@action(description="转移液体")
def transfer(self, source: ResourceSlot, target: ResourceSlot, volume_ul: float) -> dict:
"""
Args:
source[源资源]: 源容器或孔位。
target[目标资源]: 目标容器或孔位。
volume_ul[体积(ul)]: 转移体积。
"""
return {"success": True}
@action(description="同步设备")
def sync_devices(self, devices: List[DeviceSlot]) -> dict:
return {"success": True, "count": len(devices)}
```
### @topic_config — 状态属性配置
```python
@@ -105,13 +233,27 @@ 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
from unilabos.registry.decorators import action, device, not_action, topic_config
@device(id="my_device", category=["my_category"], description="设备描述")
@device(
id="my_device",
category=["my_category"],
description="设备描述",
display_name="设备显示名",
)
class MyDevice:
"""设备类说明。"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
初始化设备。
Args:
device_id[设备ID]: 设备实例 ID默认使用 my_device。
config[设备配置]: 设备启动配置。
"""
self.device_id = device_id or "my_device"
self.config = config or {}
self.logger = logging.getLogger(f"MyDevice.{self.device_id}")
@@ -133,7 +275,13 @@ class MyDevice:
@action(description="执行操作")
def my_action(self, param: float = 0.0, name: str = "") -> Dict[str, Any]:
"""带 @action 装饰器 → 注册为 'my_action' 动作"""
"""
带 @action 装饰器 → 注册为 'my_action' 动作。
Args:
param[操作数值]: 操作使用的数值参数。
name[操作名称]: 操作名称或备注。
"""
return {"success": True}
def get_info(self) -> Dict[str, Any]:
@@ -158,3 +306,154 @@ class MyDevice:
- `post_init``@not_action` 标记,参数类型标注为 `BaseROS2DeviceNode`
- 运行时状态存储在 `self.data` 字典中
- 设备文件放在 `unilabos/devices/<category>/` 目录下
---
## 通信实现片段
Serial 文本指令:
```python
def _send_command(self, cmd: str) -> str:
self.ser.write(f"{cmd}\r\n".encode())
return self.ser.readline().decode().strip()
```
RS-485 响应解析要先定位帧头,不要用硬编码索引直接解析原始响应:
```python
def _normalize_response(self, raw: str, start_marker: str = "/") -> str:
pos = raw.find(start_marker)
return raw[pos:] if pos >= 0 else raw
```
自定义二进制帧:
```python
def _build_frame(self, func_code: int, data: bytes) -> bytes:
frame = bytearray([0xFE, func_code]) + bytearray(data)
checksum = sum(frame[1:]) % 256
frame.append(checksum)
return bytes(frame)
```
Modbus 寄存器映射:
```python
REGISTER_MAP = {
"temp_target": {"addr": 0x000B, "scale": 10},
}
def set_temperature(self, temp: float, **kwargs) -> bool:
reg = REGISTER_MAP["temp_target"]
value = int(float(temp) * reg["scale"]) & 0xFFFF
self.client.write_register(reg["addr"], value, slave=self.slave_id)
self.data["temp_target"] = temp
return True
```
HTTP API 映射:
```python
API_MAP = {
"set_temperature": {
"method": "POST",
"endpoint": "/api/temperature",
"body_key": "target",
},
}
```
SDK 封装:
```python
from my_device_sdk import DeviceController
class MyDevice:
def __init__(self, device_id=None, config=None, **kwargs):
self.config = config or {}
self.controller = DeviceController(port=self.config.get("port", "COM1"))
```
---
## 验证
无需手写注册表 YAML。`@device` 装饰器 + AST 扫描会在启动或检查时生成注册表条目。
```bash
# 1. 模块可导入
python -c "from unilabos.devices.<category>.<file> import <ClassName>"
# 2. 启动测试
unilab -g <graph>.json
# 3. 仅检查注册表
unilab --check_mode --skip_env_check
```
仅在旧代码无 `@device`、需要覆盖特殊字段、或做 `--complete_registry` 旧设备补全时,才考虑 YAML。新设备默认不要手写 YAML。
## 图文件节点模板
实验图 JSON 中的 `class` 对应 `@device(id=...)``config` 会传入 `__init__``config` 字典:
```json
{
"id": "my_device_1",
"name": "我的设备",
"children": [],
"parent": null,
"type": "device",
"class": "my_device",
"position": {"x": 0, "y": 0, "z": 0},
"config": {
"port": "/dev/ttyUSB0",
"baudrate": 9600
},
"data": {}
}
```
工作站需要同时配置 `deck``children`
```json
{
"nodes": [
{
"id": "my_station",
"type": "device",
"class": "my_workstation",
"children": ["my_deck"],
"config": {},
"deck": {
"data": {
"_resource_child_name": "my_deck",
"_resource_type": "unilabos.resources.my_module:MyDeck"
}
}
},
{
"id": "my_deck",
"type": "deck",
"class": "MyDeckClass",
"parent": "my_station",
"config": {"type": "MyDeckClass", "setup": true}
}
]
}
```
---
## 常见错误清单
- 缺少 `@device`:设备不会被 AST 扫描发现。
- 只有 `@property` 没有 `@topic_config()`:属性不会稳定广播到 `status_types`
- `post_init` 没有 `@not_action`:会被误暴露为动作。
- `self.data = {}`:空字典会导致属性读取和 schema 初始数据不稳定,必须预填充每个状态键。
- 动作参数重命名:不要把同类设备已有的 `volume` 改成 `volume_ml`,参数名是接口契约。
- `status` 使用中文或临时文本:前端和工作流依赖稳定英文状态值。
- async 方法中使用 `time.sleep()`:应使用 `await self._ros_node.sleep(seconds)`
- 硬编码串口响应索引RS-485 响应前可能有噪声字节,应先定位帧头。
- 把硬件寄存器单位暴露给用户:对外使用物理单位,驱动内部做 scale 转换。

View File

@@ -0,0 +1,450 @@
---
name: filter-workflow-by-tags
description: Query backend workflow list, aggregate all tags, and filter workflows by domain/scenario requirements using tags. Use when the user wants to search workflows, find workflows by tags, list available workflow tags, filter workflows by category/domain/scenario, or mentions 工作流筛选/标签查询/workflow tags/按领域查找工作流.
---
# Uni-Lab 工作流标签筛选指南
通过 Uni-Lab 云端 API 查询工作流列表汇总所有可用标签tags并根据领域和场景要求筛选工作流。
> **重要**:本指南中的 `Authorization: Lab <token>` 是 **Uni-Lab 平台专用的认证方式**`Lab` 是 Uni-Lab 的 auth scheme 关键字,**不是** HTTP Basic 认证。请勿将其替换为 `Basic`。
## 使用模式识别
**用户可能一开始就给出场景目标**(如"我要做有机合成实验"、"找柱层析相关的 protocol")。此时:
1. **识别场景关键词** → 映射到可能的 tags如 synthesis、organic、chromatography、purification
2. **直接执行完整流程**(获取 ak/sk/addr → 拉取所有工作流 → 汇总 tags → 按场景筛选)
3. **展示筛选结果** → 引导用户从候选 workflow 中**选择明确的实验 protocol**
4. **如果用户确认某个 workflow** → 记录 `workflow_uuid`,准备对接“与其他 Skill 的协作”
**如果用户未给场景目标**,则按标准 checklist 询问筛选条件。
---
## 前置条件
使用本指南前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
### 1. ak / sk → AUTH
询问用户的启动参数,从 `--ak` `--sk` 或 config.py 中获取。
生成 AUTH token
```bash
python -c "import base64,sys; print('Authorization: Lab ' + base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" <ak> <sk>
```
### 2. --addr → BASE URL
| `--addr` 值 | BASE |
| ------------- | ------------------------------------- |
| `test` | `https://leap-lab.test.bohrium.com` |
| `uat` | `https://leap-lab.uat.bohrium.com` |
| `local` | `http://127.0.0.1:48197` |
| 不传(默认) | `https://leap-lab.bohrium.com` |
确认后设置:
```bash
BASE="<根据 addr 确定的 URL>"
AUTH="Authorization: Lab <上面命令输出的 token>"
```
### 3. lab_uuid实验室 UUID
如果用户未提供 `lab_uuid`,通过以下 API 自动获取:
```bash
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
```
返回 `data.uuid` 即为 `lab_uuid`
**三项全部就绪后才可开始。**
## Session State
在整个对话过程中agent 需要记住以下状态:
- `lab_uuid` — 实验室 UUID
- `all_workflows` — 完整工作流列表(分页获取后缓存到内存或临时文件)
- `all_tags` — 所有工作流的标签汇总
---
## API 端点
### 查询工作流列表(支持分页)
```
GET $BASE/api/v1/lab/workflow/owner/list?page=<page>&page_size=<page_size>&lab_uuid=$lab_uuid
```
**参数:**
- `page` — 页码,从 1 开始
- `page_size` — 每页数量,建议 1000
- `lab_uuid` — 实验室 UUID
**返回结构:**
```json
{
"code": 0,
"data": {
"has_more": true,
"data": [
{
"uuid": "9661bba2-1b9f-4687-a63d-910245df174b",
"name": "Untitled",
"description": "",
"user_id": "114211",
"published": false,
"tags": null
},
{
"uuid": "e0436638-190b-46bc-b1a1-2711d9602f6a",
"name": "Synthesis v2",
"user_id": "114211",
"published": true,
"tags": ["synthesis", "organic"]
}
]
}
}
```
**字段说明:**
- `has_more` — 若为 `true`,需要继续请求 `page+1`
- `tags` — 可能为 `null`、空数组或字符串数组;聚合时必须容忍 `null`
### 启动工作流(直接运行)
```
POST $BASE/api/v1/lab/workflow/<workflow_uuid>/run
```
**用途:** 直接启动一个 workflow 的默认执行(使用模板中预设的参数),无需创建 notebook。适用于快速测试或无参数变化的重复执行。
**请求体:** 空 JSON `{}` 或省略
**返回:**
```json
{
"code": 0,
"data": "<run_uuid>"
}
```
- `run_uuid` — 本次执行的唯一标识(不是 notebook UUID
**注意:**
- 该接口会使用 workflow 模板中保存的默认参数直接执行
- 如果 workflow 需要动态参数(如 CSV 路径、样品 UUID应使用 `POST /lab/notebook` 创建 notebook 并传入 `node_params`
- 返回的 `run_uuid` 可直接传入下方「查询任务状态」接口查询实时进度
### 查询任务状态
```
GET $BASE/api/v1/lab/mcp/task/<task_uuid>
```
**用途:** 查询由 `POST /lab/workflow/<uuid>/run` 返回的 `run_uuid`(即 task_uuid的实时执行状态包括整体状态和每个节点JOSJob On Station的执行详情。
**路径参数:**
- `task_uuid` — 等同于启动工作流接口返回的 `run_uuid`
**返回:**
```json
{
"code": 0,
"data": {
"status": "running",
"jos_status": [
{
"uuid": "d0e24bfe-8d99-450e-b19d-f25849dfbaad",
"node_name": "PRCXI_BioER_96_wellplate_slot_1",
"action_name": "create_resource",
"status": "success",
"return_info": {
"suc": true,
"error": "",
"return_value": { ... }
}
},
{
"uuid": "...",
"node_name": "...",
"action_name": "transfer_liquid",
"status": "pending",
"return_info": null
}
]
}
}
```
**字段说明:**
- `data.status` — 整体任务状态
- `running` — 正在执行(至少一个节点 pending 或 running
- `success` — 全部节点成功
- `failed` — 有节点失败
- `data.jos_status[]` — 节点级执行列表(按执行顺序)
- `uuid` — 节点执行实例 UUID
- `node_name` — 节点名称(资源/设备名或工位名)
- `action_name` — 动作类型(`create_resource``transfer_liquid``centrifuge`、等)
- `status` — 节点状态:`success``pending``running``failed`
- `return_info` — 执行返回,失败时 `suc=false``error` 有错误信息
**注意:**
- 此接口的 `task_uuid` **是** `POST /lab/workflow/<uuid>/run` 返回的 `run_uuid`,二者为同一个 ID 的不同称呼
- **不要**把 notebook UUID`POST /lab/notebook` 返回)传进来——那条路径用 `GET /lab/notebook/status` 查询
- `jos_status` 数组按节点执行顺序给出;从 pending 数量可以估算剩余进度
- 返回体可能较大(`return_info.return_value` 中可能包含完整 resource tree可在脚本中只提取 `status` + `node_name` + `action_name` 做摘要
**状态轮询示例:**
```bash
# 每 5 秒轮询一次直至完成
TASK="b183d97e-d2b5-4b24-b14b-820df04d87c0"
while :; do
st=$(curl -s -X GET "$BASE/api/v1/lab/mcp/task/$TASK" -H "$AUTH" \
| python3 -c "import json,sys; d=json.load(sys.stdin)['data']; \
print(d['status'], '|', sum(1 for j in d['jos_status'] if j['status']=='success'), '/', len(d['jos_status']))")
echo "$(date +%H:%M:%S) $st"
[[ "$st" == success* || "$st" == failed* ]] && break
sleep 5
done
```
---
## 完整工作流 Checklist
```
Task Progress:
- [ ] Step 0: 识别用户是否已给出场景目标(如"有机合成"、"柱层析"
- 若已给出 → 记录场景关键词,自动进入后续步骤
- 若未给出 → 在 Step 6 询问用户
- [ ] Step 1: 确认 ak/sk → 生成 AUTH token
- [ ] Step 2: 确认 --addr → 设置 BASE URL
- [ ] Step 3: GET /edge/lab/info → 获取 lab_uuid如用户未提供
- [ ] Step 4: 分页获取所有工作流(从 page=1 开始直到 has_more=false
- [ ] Step 5: 汇总所有非空 tags → 生成 all_tags去重、排序、附出现次数
- [ ] Step 6: 根据场景关键词Step 0 或新询问)在 all_tags 中做语义映射 → 确定候选 tags
- 若语义映射不唯一,列出候选 tags 让用户确认
- [ ] Step 7: 按候选 tags 筛选工作流(默认 any 模式,召回优先)
- [ ] Step 8: 展示筛选结果uuid、name、description、tags、published
- [ ] Step 9: 引导用户从结果中选择**明确的实验 protocol**
- 若结果只有 1 条 → 直接确认该 workflow_uuid
- 若结果 210 条 → 让用户按编号选择
- 若结果过多 → 提示收紧条件(加 tag、切换 all 模式、仅 published
- 若结果为空 → 放宽条件(去掉最稀有 tag或提示用户换关键词
- [ ] Step 10: 记录用户选中的 workflow_uuid并提示提交实验或查看详情
```
---
## 推荐路径:使用脚本
同目录下提供 `scripts/filter_workflows.py`,一次完成分页抓取、标签聚合与筛选:
```bash
# 1. 仅汇总标签(不筛选)
python scripts/filter_workflows.py \
--auth "<Lab base64token>" \
--base "$BASE" \
--lab-uuid "$lab_uuid" \
--summary-only
# 2. 按标签筛选ANY 模式:包含任一)
python scripts/filter_workflows.py \
--auth "<Lab base64token>" \
--base "$BASE" \
--lab-uuid "$lab_uuid" \
--tags synthesis organic \
--mode any
# 3. 按标签筛选ALL 模式:必须同时包含)
python scripts/filter_workflows.py \
--auth "<Lab base64token>" \
--base "$BASE" \
--lab-uuid "$lab_uuid" \
--tags synthesis organic \
--mode all \
--output filtered.json
# 4. 仅筛选已发布
python scripts/filter_workflows.py \
--auth "<Lab base64token>" \
--base "$BASE" \
--lab-uuid "$lab_uuid" \
--tags synthesis \
--published-only
```
**`--auth` 参数说明**:传入 `Authorization` 头中 `Lab` 之后的 base64 token不带 `Lab ` 前缀),脚本内部会自动补上 scheme。
**输出结构:**
```json
{
"total_workflows": 150,
"tag_counts": {"synthesis": 12, "organic": 8, "analysis": 5},
"all_tags": ["analysis", "organic", "synthesis"],
"filter": {"tags": ["synthesis", "organic"], "mode": "any"},
"filtered_workflows": [
{"uuid": "...", "name": "...", "description": "...", "tags": [...], "published": true}
]
}
```
---
## 手动路径curl + jq
如果脚本不可用或环境缺少 Python可用 shell 实现。
### 1. 分页抓取(写入 `all_workflows.json`
```bash
page=1
echo "[]" > all_workflows.json
while :; do
resp=$(curl -s -X GET \
"$BASE/api/v1/lab/workflow/owner/list?page=$page&page_size=1000&lab_uuid=$lab_uuid" \
-H "$AUTH")
page_data=$(echo "$resp" | jq -c '.data.data // []')
jq -c --argjson p "$page_data" '. + $p' all_workflows.json > _tmp.json && mv _tmp.json all_workflows.json
has_more=$(echo "$resp" | jq -r '.data.has_more')
[ "$has_more" != "true" ] && break
page=$((page + 1))
done
echo "Total: $(jq 'length' all_workflows.json)"
```
### 2. 汇总所有标签(含出现次数)
```bash
jq '[.[].tags // [] | .[]] | group_by(.) | map({tag: .[0], count: length}) | sort_by(-.count)' \
all_workflows.json
```
### 3. 按标签筛选
```bash
# ANY包含任一指定标签
jq --argjson want '["synthesis","organic"]' \
'[.[] | select((.tags // []) | any(. as $t | $want | index($t)))]' \
all_workflows.json
# ALL同时包含所有指定标签
jq --argjson want '["synthesis","organic"]' \
'[.[] | select(($want | all(. as $w | (.tags // []) | index($w))))]' \
all_workflows.json
```
---
## 筛选策略
agent 拿到用户的「领域 + 场景」自然语言描述时,按如下顺序选择 tag
1. **优先用户显式指定的 tags**:若用户明确给出标签词,直接精确匹配。
2. **从 all_tags 中做语义映射**:若用户描述是自然语言(如"有机合成、柱层析"),在 all_tags 中找语义相关项(如 `synthesis``organic``chromatography`)。必要时展示候选 tag 让用户确认。
3. **模式选择**
- 默认 `any`(更多召回),给出 tag 集合的并集匹配
- 用户强调"必须同时满足"时用 `all`
4. **空结果兜底**:如果筛选为空,放宽条件(去掉最稀有 tag、切换 any 模式),并提醒用户。
---
## 引导到明确的 Protocol
筛选完成后,**最终目标是让用户确认一个具体的 workflow_uuid**,而不是停留在"一堆候选"上。按结果数量采取不同策略:
| 结果数量 | 策略 |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| 0 条 | 放宽筛选(去掉最稀有 tag → 切换 any 模式 → 去掉 `--published-only`)。仍为空则提示换关键词,或列出 `all_tags` 让用户重新选。 |
| 1 条 | 直接确认:"找到唯一匹配:`<name>` (uuid `<uuid>`),是否用它?"用户确认后记录 `workflow_uuid`。 |
| 210 条 | 编号列表展示,让用户选编号。每项给出 name、tags、description 摘要、published 状态。 |
| 1030 条 | 先展示 tag 分布帮助用户进一步收紧:列出匹配结果中最常见的子标签,提示"加一个 tag 可将结果缩小到 N 条"。 |
| >30 条 | 强制要求用户补充条件:仅 published、指定具体 tag 组合、或按名称关键词过滤。 |
**确认 workflow 后**
1.`workflow_uuid` 写入 session state
2. 提示用户下一步可用的 skill
- 提交实验 → 引导到“与其他 Skill 的协作”
- 查看 workflow 详细节点 → `GET /api/v1/lab/workflow/template/detail/<workflow_uuid>`
3. 若用户想换一个,回到筛选步骤。
---
## 展示结果
推荐格式(表格 + 汇总统计):
```
共 150 个工作流,其中 32 个匹配筛选条件 [tags: synthesis OR organic]
| UUID (短) | 名称 | Tags | 已发布 |
|-----------|--------------------------|------------------------------|--------|
| e0436638 | Synthesis v2 | synthesis, organic | ✓ |
| 5b60dbb8 | Grignard Protocol | synthesis, organometallic | ✓ |
| ... | ... | ... | ... |
所有可用标签(按频次):
synthesis (12), organic (8), analysis (5), purification (4), ...
```
如果用户下一步想执行某工作流 → 引导到“与其他 Skill 的协作”。
---
## 常见问题
### Q: tags 为 null 的工作流要不要展示?
默认**不展示**在筛选结果中(因为无法按 tag 匹配)。但在 `--summary-only` 或无筛选条件时,这些工作流仍会计入总数,并在输出中单独列出"未打标签"计数。
### Q: 如何按名称/描述做模糊匹配?
脚本未内置,但可在 jq 中组合:
```bash
jq '[.[] | select((.name + " " + (.description // "")) | test("organic"; "i"))]' all_workflows.json
```
### Q: `page_size=1000` 是否会被服务端限制?
接口通常允许最大 1000如果返回量少于 1000 且 `has_more=false`,说明已到末页。极端情况下若服务端返回错误,可降到 200 或 500 再试。
### Q: 工作流数量极大(>10k怎么办
1. 先跑 `--summary-only` 了解 tag 分布
2. 提示用户先限定 `--published-only` 或指定 tag
3. 考虑将 `all_workflows.json` 缓存到本地,下次直接复用
---
## 与其他 Skill 的协作
- 正常情况下,找到 workflow 之后可以直接用它提交实验(启动工作流的 api 端点 POST $BASE/api/v1/lab/workflow/<workflow_uuid>/run不用别的 skill
- **仅当需要进行多次实验时,使用 batch-submit-experiment** — 筛选到目标工作流后,`workflow_uuid` 直接用于实验提交
## 脚本依赖
`scripts/filter_workflows.py` 仅使用 Python 标准库(`urllib``json``argparse`),无需额外安装。

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""分页拉取 Uni-Lab 工作流列表,汇总 tags 并按 tag 筛选。
使用示例:
python filter_workflows.py \
--auth <base64token> \
--base https://leap-lab.test.bohrium.com \
--lab-uuid a9059772-... \
--tags synthesis organic --mode any
仅依赖 Python 标准库。
"""
from __future__ import annotations
import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request
from collections import Counter
def fetch_all_workflows(base: str, auth_token: str, lab_uuid: str, page_size: int = 1000) -> list[dict]:
"""分页拉取所有 owner 工作流,直到 has_more=false。"""
workflows: list[dict] = []
page = 1
while True:
query = urllib.parse.urlencode(
{"page": page, "page_size": page_size, "lab_uuid": lab_uuid}
)
url = f"{base.rstrip('/')}/api/v1/lab/workflow/owner/list?{query}"
req = urllib.request.Request(
url,
headers={
"Authorization": f"Lab {auth_token}",
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
payload = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
sys.exit(f"[ERROR] HTTP {e.code} on page {page}: {e.read().decode('utf-8', 'ignore')}")
except urllib.error.URLError as e:
sys.exit(f"[ERROR] URL error on page {page}: {e.reason}")
if payload.get("code") != 0:
sys.exit(f"[ERROR] API returned non-zero code: {payload}")
data = payload.get("data") or {}
page_items = data.get("data") or []
workflows.extend(page_items)
if not data.get("has_more"):
break
page += 1
# 防御性兜底,避免接口异常导致无限循环
if page > 1000:
print(f"[WARN] page count exceeded 1000, stopping early", file=sys.stderr)
break
return workflows
def aggregate_tags(workflows: list[dict]) -> tuple[list[str], dict[str, int], int]:
"""返回 (sorted_tags, tag_counts, untagged_count)。"""
counter: Counter[str] = Counter()
untagged = 0
for wf in workflows:
tags = wf.get("tags")
if not tags:
untagged += 1
continue
for t in tags:
if isinstance(t, str) and t.strip():
counter[t.strip()] += 1
return sorted(counter.keys()), dict(counter), untagged
def filter_workflows(
workflows: list[dict],
want_tags: list[str],
mode: str,
published_only: bool,
) -> list[dict]:
"""按 tag 筛选。mode 取值 any / all。"""
want_set = {t.strip() for t in want_tags if t.strip()}
out: list[dict] = []
for wf in workflows:
if published_only and not wf.get("published"):
continue
if not want_set:
out.append(wf)
continue
tags = wf.get("tags") or []
tag_set = {t for t in tags if isinstance(t, str)}
if mode == "all":
if want_set.issubset(tag_set):
out.append(wf)
else: # any
if want_set & tag_set:
out.append(wf)
return out
def project_workflow(wf: dict) -> dict:
"""精简输出字段。"""
return {
"uuid": wf.get("uuid"),
"name": wf.get("name"),
"description": wf.get("description", ""),
"tags": wf.get("tags") or [],
"published": bool(wf.get("published")),
"user_id": wf.get("user_id"),
}
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Fetch & filter Uni-Lab workflows by tags.")
p.add_argument("--auth", required=True, help="Base64 token (the part after `Lab `).")
p.add_argument("--base", required=True, help="Base URL, e.g. https://leap-lab.test.bohrium.com")
p.add_argument("--lab-uuid", required=True, help="Lab UUID.")
p.add_argument("--tags", nargs="*", default=[], help="Tags to filter by (space separated).")
p.add_argument(
"--mode",
choices=["any", "all"],
default="any",
help="any: workflow contains at least one tag; all: workflow contains every tag.",
)
p.add_argument("--published-only", action="store_true", help="Only include published workflows.")
p.add_argument("--page-size", type=int, default=1000, help="Page size, default 1000.")
p.add_argument(
"--summary-only",
action="store_true",
help="Print tag summary without applying filter (still fetches everything).",
)
p.add_argument("--output", help="Write JSON result to this path. If omitted, print to stdout.")
return p.parse_args()
def main() -> None:
args = parse_args()
workflows = fetch_all_workflows(
base=args.base,
auth_token=args.auth,
lab_uuid=args.lab_uuid,
page_size=args.page_size,
)
sorted_tags, tag_counts, untagged = aggregate_tags(workflows)
if args.summary_only:
result = {
"total_workflows": len(workflows),
"untagged_count": untagged,
"tag_counts": tag_counts,
"all_tags": sorted_tags,
}
else:
filtered = filter_workflows(
workflows,
want_tags=args.tags,
mode=args.mode,
published_only=args.published_only,
)
result = {
"total_workflows": len(workflows),
"untagged_count": untagged,
"tag_counts": tag_counts,
"all_tags": sorted_tags,
"filter": {
"tags": args.tags,
"mode": args.mode,
"published_only": args.published_only,
},
"matched_count": len(filtered),
"filtered_workflows": [project_workflow(wf) for wf in filtered],
}
payload = json.dumps(result, ensure_ascii=False, indent=2)
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(payload)
print(f"Wrote {len(workflows)} workflows summary → {args.output}", file=sys.stderr)
else:
print(payload)
if __name__ == "__main__":
main()

View File

@@ -10,7 +10,8 @@ description: Operate Virtual Workbench via REST API — prepare materials, move
- **device_id**: `virtual_workbench`
- **Python 源码**: `unilabos/devices/virtual/workbench.py`
- **设备类**: `VirtualWorkbench`
- **动作**: 6`auto-prepare_materials`, `auto-move_to_heating_station`, `auto-start_heating`, `auto-move_to_output`, `transfer`, `manual_confirm`
- **当前纳入动作**: 5 个`auto-prepare_materials`, `auto-move_to_heating_station`, `auto-start_heating`, `auto-move_to_output`, `transfer`
- **暂跳过动作**: `manual_confirm`、扣电测试 `test`(需要启用时先从最新注册表重新提取 schema
- **设备描述**: 模拟工作台,包含 1 个机械臂(每次操作 2s独占锁和 3 个加热台(每次加热 60s可并行
### 典型工作流程
@@ -151,7 +152,8 @@ curl -s -X POST "$BASE/api/v1/lab/mcp/run/action" \
| `auto-start_heating` | `UniLabJsonCommand` |
| `auto-move_to_output` | `UniLabJsonCommand` |
| `transfer` | `UniLabJsonCommandAsync` |
| `manual_confirm` | `UniLabJsonCommand` |
> `manual_confirm` 和扣电测试 `test` 当前不纳入本 skill 的推荐操作范围;不要基于历史 JSON 直接调用,需先重新生成并校验 schema。
### 10. 查询任务状态
@@ -225,11 +227,9 @@ curl -s -X PUT "$BASE/api/v1/edge/material/node" \
| `transfer` | `resource` | ResourceSlot | 待转移物料数组 |
| `transfer` | `target_device` | DeviceSlot | 目标设备路径 |
| `transfer` | `mount_resource` | ResourceSlot | 目标孔位数组 |
| `manual_confirm` | `resource` | ResourceSlot | 确认用物料数组 |
| `manual_confirm` | `target_device` | DeviceSlot | 确认用目标设备 |
| `manual_confirm` | `mount_resource` | ResourceSlot | 确认用目标孔位数组 |
> `prepare_materials`、`move_to_heating_station`、`start_heating`、`move_to_output` 这 4 个动作**无 Slot 字段**,参数为纯数值/整数。
> `manual_confirm` 先跳过,不维护其 Slot 字段表。
---
@@ -270,3 +270,13 @@ prepare_materials (count=5)
```
创建节点时,`prepare_materials` 的 5 个 output handle`channel_1` ~ `channel_5`)分别连接到 5 个 `move_to_heating_station` 节点的 `material_input` handle。每个 `move_to_heating_station``heating_station_output``material_number_output` 连接到对应 `start_heating``station_id_input``material_number_input`
`start_heating` 完成后还需要继续连接到 `move_to_output`,否则加热完成的物料不会移出加热台:
| source action | source handle | target action | target handle | 传递参数 |
| ------------- | ------------- | ------------- | ------------- | -------- |
| `auto-prepare_materials` | `channel_N` | `auto-move_to_heating_station` | `material_input` | `material_number` |
| `auto-move_to_heating_station` | `heating_station_output` | `auto-start_heating` | `station_id_input` | `station_id` |
| `auto-move_to_heating_station` | `material_number_output` | `auto-start_heating` | `material_number_input` | `material_number` |
| `auto-start_heating` | `heating_done_station` | `auto-move_to_output` | `output_station_input` | `station_id` |
| `auto-start_heating` | `heating_done_material` | `auto-move_to_output` | `output_material_input` | `material_number` |

View File

@@ -1,6 +1,8 @@
# Action Index — virtual_workbench
6 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/<name>.json`
当前纳入 5 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/<name>.json`
暂跳过:`manual_confirm`、扣电测试 `test`。这两个动作需要启用时,先从最新 `req_device_registry_upload.json` 重新提取 schema 并校验参数。
---
@@ -60,17 +62,18 @@
---
## 人工确认
## 暂跳过动作
### `manual_confirm`
创建人工确认节点,等待用户手动确认后继续(含物料转移上下文)
创建人工确认节点,等待用户手动确认后继续(含物料转移上下文)。当前先不纳入推荐操作范围。
- **action_type**: `UniLabJsonCommand`
- **Schema**: [`actions/manual_confirm.json`](actions/manual_confirm.json)
- **核心参数**: `resource`, `target_device`, `mount_resource`, `timeout_seconds`, `assignee_user_ids`
- **占位符字段**:
- `resource`**ResourceSlot**,物料数组
- `target_device`**DeviceSlot**,目标设备路径
- `mount_resource`**ResourceSlot**,目标孔位数组
- `assignee_user_ids``unilabos_manual_confirm` 类型
- **状态**: 暂跳过。源码参数已包含扣电测试相关字段,历史 JSON 可能过期;需要启用时重新提取 schema。
### `test`
启动扣电测试。当前先不纳入本 skill。
- **状态**: 暂跳过。需要启用时从注册表生成 `actions/test.json` 后再补充索引。

View File

@@ -1,483 +0,0 @@
---
name: yibin-electrolyte-submit
description: >-
通过 Uni-Lab Notebook API 向宜宾电解液工站提交实验覆盖配液分液Bioyond LIMS
扣电组装CoinCellAssembly、扣电测试全流程。
包含 Excel 解析、formulation 构建、工作流节点参数填写、notebook 提交与状态轮询。
Use when the user wants to submit electrolyte experiments, assemble or test coin cells,
parse experiment Excel files, build notebook payloads, or mentions
宜宾/配液/分液/扣电/电解液实验/notebook提交/CoinCell/BioyondLIMS.
---
# 宜宾电解液产线 API 操作指南
本 skill 覆盖两个设备的完整操作流程:
1. **配液分液工站** (`bioyond_cell_workstation`) — Bioyond LIMS 配液/分液/转运
2. **扣电组装站** (`BatteryStation`) — Modbus PLC 扣电组装/数据采集
## 设备信息
| 属性 | 配液分液工站 | 扣电组装站 |
|------|------------|-----------|
| device_id | `bioyond_cell_workstation` | `BatteryStation` |
| 显示名 | 配液分液工站 | 扣电工作站 |
| 源码 | `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py` | `unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py` |
| 类名 | `BioyondCellWorkstation` | `CoinCellAssemblyWorkstation` |
| 通讯 | HTTP REST (Bioyond LIMS API) | Modbus TCP (PLC 寄存器) |
## 前置条件
### 认证信息
```
AUTH="Authorization: Lab OTdlY2FkNmUtZmZmMi00YjhiLThhOWEtNWM5ODAyOTJmOTUxOmU0OGM2YWJkLTA4ZmEtNDFjMy04NzhhLTc4M2FiODlhZjYxMw=="
BASE="https://uni-lab.test.bohrium.com"
```
来源:`--ak 97ecad6e-fff2-4b8b-8a9a-5c980292f951 --sk e48c6abd-08fa-41c3-878a-783ab89af613 --addr test`
### 启动 unilab云端模式
> **重要**:提交实验前必须确保 unilab 正在运行且已连接云端 WebSocket。
```powershell
$env:PYTHONIOENCODING="utf-8"
conda activate newunilab2603
cd D:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation
unilab -g D:\UniLabdev\Uni-Lab-OS\yibin_electrolyte_config.json --ak 97ecad6e-fff2-4b8b-8a9a-5c980292f951 --sk e48c6abd-08fa-41c3-878a-783ab89af613 --upload_registry --addr test --disable_browser --skip_env_check
```
**启动要点**
1. 必须先激活虚拟环境 `newunilab2603`
2. 工作目录切到 `unilabos/devices/workstation`(设备驱动所在目录)
3. `--upload_registry` 将 64 个设备 + 142 个资源注册到云端
4. `--skip_env_check` + `PYTHONIOENCODING=utf-8` 避免 Windows GBK 编码崩溃
5. 启动后后台运行,等待日志出现 `Application startup complete``Host node ready signal published with 3 devices`
**验证连接成功的标志**
- 日志出现 `[MessageProcessor] ... wss://uni-lab.test.bohrium.com/api/v1/ws/schedule`
- 日志出现 `[WebSocketClient] Host node ready signal published with 3 devices`
- 日志出现 `Resource tree add completed`(资源树同步完成)
### 云端物料上架与入库(启动后必做)
> **在提交实验之前,必须提醒用户完成以下云端操作,否则实验会因物料缺失而失败。**
1. **拖拽上料**:在云端 UI`$BASE/laboratory/<lab_uuid>`)的资源树视图中,将物料拖拽到对应的仓库/库位上。unilab 启动后资源树会自动同步到云端,但物料的**上架位置**需要用户在 UI 上手动确认或调整。
2. **确认配液物料入库**:确保所有配液实验需要的试剂(如 LiPF6、EC、DMC、EMC 等)已在 LIMS 系统中完成入库。可通过以下方式验证:
- 云端 UI 资源树中对应仓库(如"粉末加样头堆栈"、"配液站内试剂仓库")下有物料节点
- 或通过 API #8 获取资源树后检查物料节点是否存在
3. **告知 AI 可以提交**:用户完成上述操作后,告知 AI "物料已上架,可以提交实验"AI 再执行 notebook 提交流程。
**提醒话术模板**AI 应在启动成功后发送给用户):
```
unilab 已成功启动并连接云端。提交实验前请完成以下操作:
1. 在云端 UI 上确认资源树中的物料位置,必要时拖拽调整上料位
2. 确保配液所需的试剂(粉末、液体)已在 LIMS 中完成入库
3. 完成后告诉我,我将为您提交实验
```
### 生成 Action Schema首次使用
启动 unilab 后,在 `unilabos_data/` 目录下会生成 `req_device_registry_upload.json`。运行以下命令提取两个设备的 action JSON
```bash
python .cursor/skills/create-device-skill/scripts/extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json bioyond_cell_workstation .cursor/skills/yibin-electrolyte-submit/actions/
python .cursor/skills/create-device-skill/scripts/extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json BatteryStation .cursor/skills/yibin-electrolyte-submit/actions/
```
## 请求约定
- Windows 平台**必须用 `curl.exe`**(非 PowerShell 的 curl 别名)
- 所有请求带 `$AUTH`
- URL 格式:`$BASE/api/v1/<endpoint>`
- POST/PATCH 请求体写入临时 JSON 文件后用 `-d '@tmp.json'` 传参(避免 PowerShell 转义问题)
- 本地 API 基址:`http://127.0.0.1:8002/api/v1/`
## Session State
每次会话开始时,依次获取以下信息:
```bash
# 1. lab_uuid
curl.exe -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
# → data.uuid → $lab_uuid
# 2. project_uuid
curl.exe -s -X GET "$BASE/api/v1/lab/project/list?lab_uuid=$lab_uuid" -H "$AUTH"
# → data.items[].uuid/name → 让用户选择或取唯一项 → $project_uuid
```
## 工作流模板(重要)
> **必须向用户索要已有的工作流模板 UUID 或 URL不要自行创建。**
>
> 原因:通过 `edge/workflow/node` API 创建节点会报 `resource_node_template not found`——
> 云端的工作流节点模板系统和设备注册表是独立的,需要用户在云端 UI 上预先配置好工作流模板。
**获取方式**
- 用户提供工作流页面 URL`$BASE/laboratory/<lab_uuid>/workflow/<workflow_uuid>`
- 从 URL 中提取 `workflow_uuid`
- 用 API 获取模板详情:
```
GET /api/v1/lab/workflow/template/detail/<workflow_uuid>
```
返回 `data.nodes[]`:每个节点的 uuid、name、param、device_name、handles、disabled。
**示例**
```
工作流 URL: https://uni-lab.test.bohrium.com/laboratory/e9ed9102-d709-4741-b7a0-d1e8578e2065/workflow/b49f80d9-58d6-4456-a521-56f4dd39cda0
→ workflow_uuid = b49f80d9-58d6-4456-a521-56f4dd39cda0
```
从模板详情中提取**未 disabled** 的节点的 `uuid``name`,后续提交 notebook 时使用。
## API Endpoints
### #1 获取 lab_uuid
```
GET /api/v1/edge/lab/info
```
### #2 列出项目
```
GET /api/v1/lab/project/list?lab_uuid=$lab_uuid
```
返回 `data.items[]`,取 `uuid``name`
### #3 获取工作流模板详情
```
GET /api/v1/lab/workflow/template/detail/<workflow_uuid>
```
返回 `data.nodes[]`:每个节点的 uuid、name、param、device_name、handles。
提取活跃节点(`disabled != true`)的 `uuid` 用于构建 notebook 请求。
### #4 提交实验(创建 notebook— 核心 API
```
POST /api/v1/lab/notebook
Body: {
"lab_uuid": "<lab_uuid>",
"project_uuid": "<project_uuid>",
"workflow_uuid": "<workflow_uuid>",
"name": "<实验名称>",
"node_params": [
{
"sample_uuids": [],
"datas": [
{
"node_uuid": "<模板中的节点UUID>",
"param": { <参数键值对> },
"sample_params": []
}
]
}
]
}
```
**关键注意事项**
- `node_params` 是数组,每个元素代表一轮实验
- `datas` 中每个节点对应模板中的一个活跃节点
- `param` 中的字段名**必须使用 Python 函数参数名**,不能用模板中存储的 LIMS 字段名(见下方映射表)
### #5 查询 notebook 状态
```
GET /api/v1/lab/notebook/status?uuid=<notebook_uuid>
```
| status | 含义 |
|--------|------|
| `running` | 执行中 |
| `success` | 成功 |
| `fail` | 失败 |
### #6 运行设备单动作(本地 API
```
POST http://127.0.0.1:8002/api/v1/job/add
Body: {
"device_id": "<device_id>",
"action": "<action_name>",
"action_args": { <参数键值对> },
"sample_material": {}
}
```
本地 API 可自动解析 `action_type`,无需手动指定。适用于快速调试或云端未连接时。
### #7 查询本地任务状态
```
GET http://127.0.0.1:8002/api/v1/job/<job_id>/status
```
| status | 含义 |
|--------|------|
| 0 | UNKNOWN |
| 1 | ACCEPTED |
| 2 | EXECUTING |
| 4 | SUCCEEDED |
| 5 | CANCELED |
| 6 | ABORTED |
### #8 获取资源树
```
GET /api/v1/lab/material/download/<lab_uuid>
```
返回所有节点(`id`, `name`, `uuid`, `type`, `parent`)。填写 Slot 字段时用此接口筛选节点。
## Placeholder Slot 填写规则
action JSON 中 `placeholder_keys` 标记了哪些字段需要填 Slot
| placeholder 值 | Slot 类型 | 填写格式 |
|---------------|-----------|---------|
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` |
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` 路径字符串 |
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` 路径字符串 |
| `unilabos_class` | ClassSlot | `"class_name"` 字符串 |
| `unilabos_formulation` | FormulationSlot | `[{well_name, liquids: [{name, volume}]}]` |
### ResourceSlot 填写
从 API #8 资源树中筛选**物料**节点:
```json
{"id": "/bioyond_cell_workstation/YB_Bioyond_Deck/自动堆栈-左", "name": "自动堆栈-左", "uuid": "3a19debc-..."}
```
数组字段:`[{id, name, uuid}, ...]`
特例:`create_resource``res_id` 允许填不存在的路径。
### DeviceSlot 填写
从资源树筛选 `type=device` 的节点,填路径字符串:
```
"/BatteryStation"
"/bioyond_cell_workstation"
```
### FormulationSlot 填写
```json
[
{
"sample_uuid": "",
"well_name": "YB_PrepBottle_15mL_Carrier_bottle_A1",
"liquids": [
{ "name": "LiPF6", "mass": 12.5 },
{ "name": "EC", "mass": 50.0 }
]
}
]
```
`well_name` 从资源树中取物料节点的 `name`
## 参数名映射(重要的坑)
> 工作流模板中存储的参数名和 Python 函数实际接受的参数名**不一定相同**。
> 提交 notebook 时必须使用 **Python 函数参数名**。
### `create_orders_formulation` 参数映射
| 模板中的 param 键 | 实际 Python 参数名 | 说明 |
|-------------------|-------------------|------|
| `pouch_cell_info` | `pouch_cell_volume` | 软包组装分液体积 (mL) |
| `conductivity_info` | `conductivity_volume` | 电导测试分液体积 (mL) |
| `load_shedding_info` | `coin_cell_volume` | 扣电组装分液体积 (mL) |
| `formulation` | `formulation` | 配方数组(名称一致) |
| `batch_id` | `batch_id` | 批次号(名称一致) |
| `bottle_type` | `bottle_type` | 配液瓶类型(名称一致) |
| `mix_time` | `mix_time` | 混匀时间(秒)(名称一致) |
| `conductivity_bottle_count` | `conductivity_bottle_count` | 电导瓶数(名称一致) |
当从模板中读到 `param` 包含 `pouch_cell_info` 等 LIMS 字段名时,提交 notebook 时要用右列的 Python 函数参数名。否则会报 `TypeError: got an unexpected keyword argument`
## 典型工作流
### 方式一:通过 Notebook API 批量提交(推荐)
**适用场景**:多组配方的批量实验,云端管理实验记录
```
1. 向用户索要工作流模板 URL不要自行创建
2. 获取 lab_uuidAPI #1和 project_uuidAPI #2
3. 获取工作流模板详情API #3提取活跃节点 UUID
4. 解析用户提供的 Excel 文件,构建 formulation 数组
5. 提交 notebookAPI #4
6. 轮询 notebook 状态API #5直到完成
```
**Excel 解析规则**
- 全局参数在第一个数据行:`batch_id``bottle_type``mix_time``coin_cell_volume``pouch_cell_volume``conductivity_volume``conductivity_bottle_count`
- 配方列从"试剂名1"开始,交替排列:试剂名列 + 质量列(以 `(g)` 结尾)
- 每行一个配方,`order_name` = 配方ID列
- formulation 中每个配方的 materials 数组只包含 `mass > 0` 的试剂
**node_params 构建**:所有配方放入同一个 round 的同一个 datas 条目中,因为只有一个节点(`create_orders_formulation`)。
### 方式二:设备单步操作(本地 API
**适用场景**:调试、快速测试
```
1. 确保 unilab 已在本地启动
2. 通过 POST http://127.0.0.1:8002/api/v1/job/add 提交任务
3. 通过 GET /api/v1/job/<job_id>/status 查询状态
```
### 设备操作流程:配液 → 转运 → 扣电
```
1. [配液站] scheduler_start_and_auto_feeding → 启动调度 + 上料
2. [配液站] create_orders_formulation → 创建配液实验(配方输入)
3. [配液站] transfer_3_to_2_to_1_auto → 分液瓶板转运到扣电站
4. [扣电站] func_pack_device_init_auto_start_combined → 初始化+自动+启动
5. [扣电站] func_sendbottle_allpack_multi → 发送瓶数+批量组装
```
## 云端使用心得
### 环境准备
- Windows 必须设置 `$env:PYTHONIOENCODING="utf-8"` 防止编码崩溃
- 使用 `--skip_env_check` 跳过依赖检查,加快启动
- 工作目录建议在 `unilabos/devices/workstation` 下启动
### 连接与注册
- `--upload_registry` 会自动将设备和资源注册到云端
- WebSocket 连接建立后,本地和云端的资源树会自动同步
- 注册成功后用户需在云端 UI 完成**物料拖放上架**操作
- 如果 unilab 断开重连,资源树会重新同步
### 工作流模板
- **不要自行调用 API 创建工作流或节点**——云端工作流节点模板需要预配置
- 始终向用户索要已有的工作流模板 URL
- 从 URL 中提取 `workflow_uuid`,通过 API #3 获取详情
- 模板中 `disabled: true` 的节点跳过,只处理活跃节点
### Notebook 实验提交
- Notebook 是云端管理实验的标准方式
- 一个 notebook 可包含多轮(`node_params` 数组),每轮可包含多组参数
- 提交后通过 API #5 轮询状态LIMS 配液流程通常需要较长时间8 个配方约 30-60 分钟)
- 实验进度可在云端 UI 和本地 unilab 日志中同步查看
### 常见错误
| 错误 | 原因 | 解决 |
|------|------|------|
| `edge not started error` | unilab 未连接云端 WebSocket | 检查 unilab 是否在运行、重启 |
| `resource_node_template not found` | 云端没有该设备的工作流模板 | 向用户索要已有模板,不要自行创建 |
| `got an unexpected keyword argument` | 参数名用了模板字段名而非 Python 函数参数名 | 参照上方映射表转换 |
| `UnicodeEncodeError: 'gbk'` | Windows 默认编码不支持特殊字符 | 设置 `PYTHONIOENCODING=utf-8` |
| `parse parameter error` | 云端 API 字段名错误 | `device_id` (非 `device_name`)、`action` (非 `action_name`)、必须带 `action_type` |
## 渐进加载策略
1. 先读本文件了解 API 端点、参数映射和云端注意事项
2. 需要具体 action 参数时,读 [action-index.md](action-index.md) 查找 action 名称和核心参数
3. 需要完整 schema 时,读 `actions/<action_name>.json`(需先运行提取命令生成)
4. 需要理解参数含义时,读设备源码
## 完整 Notebook 提交 Checklist
```
- [ ] 确认 unilab 已在本地启动并连接云端 WebSocket
- [ ] 提醒用户在云端 UI 拖拽上料、确认物料位置
- [ ] 提醒用户确认配液所需试剂已在 LIMS 完成入库
- [ ] 等待用户确认物料就绪后再继续
- [ ] 向用户索要工作流模板 URL → 提取 workflow_uuid
- [ ] 获取 lab_uuidAPI #1
- [ ] 获取 project_uuidAPI #2
- [ ] 获取工作流模板详情API #3提取活跃节点 UUID
- [ ] 解析用户 Excel 文件 → 构建 formulation + 全局参数
- [ ] 注意参数名映射(模板字段名 → Python 函数参数名)
- [ ] 提交 notebookAPI #4
- [ ] 轮询 notebook 状态API #5直到完成
```
---
## 真实场景:宜宾产线 Excel 提交提示词模板
> 以下为已验证可用的标准提示词,适用于配液-分液-扣电全流程。
### 场景说明
- unilab 运行在本地 Windows 机器miniforge 环境),连接云端 WebSocket
- AICursor / OpenClaw在任意设备上通过云端 API 操作,**不需要本地 127.0.0.1**
- 工作流为 5 节点串联:`create_orders_formulation``transfer_3_to_2_to_1_auto``func_pack_device_init_auto_start_combined``func_sendbottle_allpack_multi``transfer_1_to_2`
### 已知固定参数(宜宾产线)
```
BASE = https://uni-lab.test.bohrium.com
lab_uuid = e9ed9102-d709-4741-b7a0-d1e8578e2065
project = YiBinElectrolyte (bc5224b4-8120-4765-9961-9dfc1802a1f6)
workflow = 配液分液formulation全流程 (2bc59938-db79-4415-ac2d-9897ef125f2f)
```
#### 工作流节点 UUID固定无需重新查询
| 顺序 | action | node_uuid |
|------|--------|-----------|
| Step1 | auto-create_orders_formulation | `ece6744a-81ac-4ae4-8cd1-1c8eeda1dab6` |
| Step2 | auto-transfer_3_to_2_to_1_auto | `1c37a8dd-5ba0-413d-81db-94b9c936a171` |
| Step3 | auto-func_pack_device_init_auto_start_combined | `97a676a2-d257-4479-9096-073b40300970` |
| Step4 | auto-func_sendbottle_allpack_multi | `cf69017a-d29c-4aad-a63b-309d63dac2e9` |
| Step5 | auto-transfer_1_to_2 | `80d1c1aa-dbc3-4601-86b7-5c22a992dd9e` |
### 标准提示词
```
请使用 yibin-electrolyte-submit skill提交以下实验
工作流模板 URLhttps://uni-lab.test.bohrium.com/laboratory/e9ed9102-d709-4741-b7a0-d1e8578e2065/workflow/2bc59938-db79-4415-ac2d-9897ef125f2f
Excel 文件路径:<粘贴或上传 xlsx 路径>
注意事项:
- lab_uuid、project_uuid、workflow节点UUID均已固定无需重新查询
- 直接解析 Excel → 构建 payload → 提交
- mix_time 传标量整数即可(已兼容)
- 试剂名以 Excel 为准,注意区分 LiDFOB / LiDOFB 等拼写
- csv_export_path 取 Excel 中 csv_export_path 列的值
- 提交后告知 notebook UUID无需自动轮询实验耗时较长
```
### Excel 列结构说明experment_template_0415sim-*.xlsx
| 列范围 | 内容 |
|--------|------|
| C | batch_id |
| D | bottle_type |
| E-H | coin_cell_volume / conductivity_bottle_count / conductivity_volume / csv_export_path |
| I-T | 试剂名+质量 交替排列最多6对|
| U | mix_time |
| V | order_name每行配方的订单号|
| W | pouch_cell_volume |
| X-Y | target_device / target_locationStep2参数|
| AA | material_search_enableStep3参数|
| AB-AS | 扣电站参数Step4|
### CSV 导出说明
每次 `create_orders_formulation` 完成后,在 `csv_export_path` 目录下生成:
```
electrolyte_orders_<YYYYMMDD_HHMMSS>.csv
```
列:`orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间`
> **注意**barCode 为 `null` 或 `"nullBarCode123456"` 是正常现象,表示 LIMS 中该物料尚未扫码。配液瓶缺失通常是因为物料未放在手动传递窗(`locationId` 前缀 `3a19deae-2c7a-`)。

View File

@@ -1,295 +0,0 @@
# Action 索引
> Action JSON 文件需运行提取命令生成,详见 [SKILL.md](SKILL.md) 中「生成 Action Schema」。
> 以下描述和参数信息基于源码分析。
---
## 配液分液工站 (`bioyond_cell_workstation`)
源码:`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
### 调度控制
#### `scheduler_start`
启动 Bioyond LIMS 调度系统
- **核心参数**: 无(仅需 apiKey/requestTime由设备内部处理
- **返回**: LIMS 响应 `{code, message, data}`
#### `scheduler_stop`
停止调度
- **核心参数**: 无
#### `scheduler_continue`
继续调度(从暂停状态恢复)
- **核心参数**: 无
#### `scheduler_reset`
复位调度
- **核心参数**: 无
#### `scheduler_start_and_auto_feeding`
**组合操作**:启动调度 + 自动化上料4号→3号手套箱
- **核心参数**: `xlsx_path`Excel 物料模板路径,可选)
- **可选参数**: WH4 加样头面 12 个点位materialName + quantity、WH4 原液瓶面 9 个点位materialName + quantity + materialType + targetWH、WH3 人工堆栈 15 个点位materialType + materialId + quantity
- **流程**: 先 `scheduler_start()`,成功后执行 `auto_feeding4to3()`
- **备注**: 支持 Excel 模式和手动参数模式Excel 路径存在时优先使用 Excel
### 物料上料/下料
#### `auto_feeding4to3`
自动化上料:从 4 号手套箱转运物料到 3 号手套箱
- **核心参数**: `xlsx_path`Excel 物料模板路径)
- **可选参数**: 同 `scheduler_start_and_auto_feeding` 的 WH4/WH3 点位参数
- **返回**: 等待上料任务完成后返回结果
#### `auto_batch_outbound_from_xlsx`
自动化下料(从 Excel 读取下料信息)
- **核心参数**: `xlsx_path`Excel 下料模板)
- **Excel 列**: locationId, warehouseId, 数量, x, y, z
### 物料管理
#### `create_and_inbound_materials`
批量创建固体物料并入库
- **核心参数**: `material_names`(物料名称列表,默认 `["LiPF6", "LiDFOB", "DTD", "LiFSI", "LiPO2F2"]`
- **可选参数**: `type_id`物料类型ID, `warehouse_name`(目标仓库,默认 "粉末加样头堆栈"
- **流程**: 创建物料 → 批量入库 → 同步
#### `create_material`
创建单个物料并可选入库
- **核心参数**: `material_name`, `type_id`, `warehouse_name`
- **可选参数**: `location_name_or_id`(库位编号如 "A01" 或 UUID
#### `create_sample`
创建配液板物料(含子瓶)并入库
- **核心参数**: `name`, `board_type`(如 "5ml分液瓶板", `bottle_type`(如 "5ml分液瓶", `location_code`(如 "A01"
- **可选参数**: `warehouse_name`(默认 "手动堆栈"
- **备注**: 自动创建 2x4=8 个子瓶
#### `storage_inbound`
单个物料入库
- **核心参数**: `material_id`, `location_id`
#### `storage_batch_inbound`
批量物料入库
- **核心参数**: `items``[{materialId, locationId}, ...]`
### 配液实验
#### `create_orders`
从 Excel 文件创建配液实验订单
- **核心参数**: `xlsx_path`Excel 文件路径)
- **Excel 列**: 配方ID, 创建日期, 配液瓶类型, 混匀时间(s), 扣电组装分液体积, 软包组装分液体积, 电导测试分液体积, 电导测试分液瓶数, 以及所有以 `(g)` 结尾的物料列
- **流程**: 解析 Excel → 提交订单 → 等待全部完成 → 计算质量比 → 提取分液瓶板 → 创建资源树对象
- **返回**: `{status, total_orders, bottle_count, reports, mass_ratios, vial_plates}`
#### `create_orders_formulation`
从配方列表创建配液实验订单(前端/API 输入版本)
- **核心参数**: `formulation`(配方数组)
- **可选参数**: `batch_id`, `bottle_type`(默认 "配液小瓶", `mix_time`(秒,列表), `coin_cell_volume`, `pouch_cell_volume`, `conductivity_volume`, `conductivity_bottle_count`
- **formulation 格式**:
```json
[
{
"order_name": "配方A",
"materials": [
{"name": "LiPF6", "mass": 12.5},
{"name": "EC", "mass": 50.0},
{"name": "DMC", "mass": 37.5}
]
}
]
```
- **返回**: 同 `create_orders`
### 物料转运
#### `transfer_3_to_2_to_1_auto`
**自动转运**:从 create_orders 结果中自动定位分液瓶板并转运到目标设备
- **核心参数**: `vial_plates`(分液瓶板列表,来自 create_orders 返回的 `vial_plates`
- **可选参数**: `target_device`(默认 "BatteryStation", `target_location`(默认 "bottle_rack_6x2", `mass_ratios`(配方信息)
- **流程**: 遍历瓶板 → 解析 locationId → 调用 LIMS 转运 API → 更新资源树
- **返回**: `{total, success, failed, results}`
#### `transfer_3_to_2_to_1`
3→2→1 物料转运(手动指定坐标)
- **核心参数**: `source_wh_id`, `source_x`, `source_y`, `source_z`
#### `transfer_3_to_2`
3→2 物料转运
- **核心参数**: `source_wh_id`, `source_x`, `source_y`, `source_z`
#### `transfer_1_to_2`
1→2 物料转运
- **核心参数**: 无
### 查询
#### `order_list_v2`
批量查询实验报告
- **可选参数**: `timeType`, `beginTime`, `endTime`, `status`60=运行中, 80=完成, 90=失败), `filter`, `skipCount`, `pageCount`, `sorting`
---
## 扣电组装站 (`BatteryStation`)
源码:`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`
### 设备控制(组合操作)
#### `func_pack_device_init_auto_start_combined`
**组合操作**:设备初始化 → 物料搜寻确认 → 切换自动模式 → 启动
- **核心参数**: `material_search_enable`(是否启用物料搜寻,默认 `False`
- **前置检查**: REG_UNILAB_INTERACT=False, COIL_GB_L_IGNORE_CMD=False, 所有握手寄存器无残留
- **流程**: 手动模式 → 初始化命令 → 监测物料搜寻弹窗并自动处理 → 自动模式 → 启动
- **返回**: `True`/`False`
- **备注**: 第一次运行必须调用此函数;后续批次调用 `func_sendbottle_allpack_multi`
### 批量组装
#### `func_sendbottle_allpack_multi`
**发送瓶数 + 批量组装**(适用于第二批次及后续批次)
- **核心参数**: `elec_num`(电解液瓶数), `elec_use_num`(每瓶组装电池数), `elec_vol`(电解液吸液量 μL默认 50
- **可选参数**:
- 双滴模式:`dual_drop_mode`(bool), `dual_drop_first_volume`(μL), `dual_drop_suction_timing`(bool), `dual_drop_start_timing`(bool)
- 组装参数:`assembly_type`(7=不用铝箔垫/8=用), `assembly_pressure`(N默认 4200)
- 物料参数:`fujipian_panshu`, `fujipian_juzhendianwei`, `gemopanshu`, `gemo_juzhendianwei`, `qiangtou_juzhendianwei`
- 开关:`lvbodian`(铝箔垫片), `battery_pressure_mode`(压力模式), `battery_clean_ignore`(忽略清洁)
- 其他:`file_path`(CSV保存路径), `formulations`(配方信息用于CSV追溯)
- **流程**: 发送瓶数触发物料搬运 → 设置PLC参数 → 循环等待PLC请求→下发参数→读取电池数据→写入CSV→更新资源树→ 完成握手
- **返回**: `{success, total_batteries, batteries, summary}`
- **备注**: 设备已初始化后直接调用;`formulations` 来自 create_orders 的 `mass_ratios`
#### `func_allpack_cmd`
全套组装(基础版本,含断点续传)
- **核心参数**: `elec_num`, `elec_use_num`, `elec_vol`, `assembly_type`, `assembly_pressure`, `file_path`
- **返回**: `{success, total_batteries, batteries, summary}`
#### `func_allpack_cmd_simp`
增强版组装(含双滴模式 + 负极片/隔膜/枪头参数)
- **核心参数**: 同 `func_sendbottle_allpack_multi`
- **备注**: 被 `func_sendbottle_allpack_multi` 内部调用
### 设备控制(单步操作)
#### `func_pack_device_init`
设备初始化(手动模式 → 初始化 → 复位标志)
#### `func_pack_device_auto`
切换自动模式
#### `func_pack_device_start`
启动设备
#### `func_pack_device_stop`
设备停止
#### `func_pack_send_bottle_num`
发送电解液瓶数(触发物料搬运)
- **核心参数**: `bottle_num`(瓶数)
### PLC 参数设置
#### `qiming_coin_cell_code`
设置组装物料参数
- **核心参数**: `fujipian_panshu`(负极片盘数)
- **可选参数**: `fujipian_juzhendianwei`, `gemopanshu`, `gemo_juzhendianwei`, `lvbodian`, `battery_pressure_mode`, `battery_pressure`, `battery_clean_ignore`
### 数据采集
#### `func_read_data_and_output`
持续数据采集并导出 CSV后台循环运行
- **核心参数**: `file_path`CSV 保存目录)
- **采集字段**: 开路电压, 极片质量, 组装时间, 压制力, 电解液加注量, 电池类型, 电解液二维码, 电池二维码
#### `func_stop_read_data`
停止 CSV 数据采集
### 设备状态属性(只读)
| 属性 | 类型 | 说明 |
|------|------|------|
| `sys_status` | str | 设备状态(启动中/停止中/复位中/初始化中) |
| `sys_mode` | str | 设备模式(手动/自动) |
| `data_assembly_coin_cell_num` | int | 已完成电池数量 |
| `data_assembly_time` | float | 单颗电池组装时间(秒) |
| `data_open_circuit_voltage` | float | 开路电压(V) |
| `data_pole_weight` | float | 正极片称重(g) |
| `data_glove_box_pressure` | float | 手套箱压力(mbar) |
| `data_glove_box_o2_content` | float | 手套箱氧含量(ppm) |
| `data_glove_box_water_content` | float | 手套箱水含量(ppm) |
| `data_coin_cell_code` | str | 电池二维码 |
| `data_electrolyte_code` | str | 电解液二维码 |
---
## 配置参考
设备图文件 `yibin_electrolyte_config.json` 中的仓库映射(`warehouse_mapping`
| 仓库名称 | 说明 | 典型操作 |
|---------|------|---------|
| 粉末加样头堆栈 | 20 个点位 (A01-T01) | `create_and_inbound_materials` 入库目标 |
| 配液站内试剂仓库 | 9 个点位 (A01-C03) | 试剂存储 |
| 自动堆栈-左 | 4 个点位 | 分液瓶板存放,`transfer_3_to_2_to_1_auto` 的源位置 |
| 自动堆栈-右 | 4 个点位 | 分液瓶板存放 |
| 手动传递窗左/右 | 各 15 个点位 | 人工上料/下料 |
| 4号手套箱内部堆栈 | 12 个点位 | `auto_feeding4to3` 的源位置 |

View File

@@ -25,7 +25,7 @@ jobs:
fetch-depth: 0
- name: Setup Miniforge
uses: conda-incubator/setup-miniconda@v3
uses: conda-incubator/setup-miniconda@v4
with:
miniforge-version: latest
use-mamba: true
@@ -38,7 +38,7 @@ jobs:
- name: Install ROS dependencies, uv and unilabos-msgs
run: |
echo Installing ROS dependencies...
mamba install -n check-env conda-forge::uv conda-forge::opencv robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs robostack-staging::ros-humble-nav2-msgs uni-lab::ros-humble-unilabos-msgs robostack-staging::ros-humble-cv-bridge robostack-staging::ros-humble-vision-opencv robostack-staging::ros-humble-tf-transformations robostack-staging::ros-humble-moveit-msgs robostack-staging::ros-humble-tf2-ros robostack-staging::ros-humble-tf2-ros-py conda-forge::transforms3d -c robostack-staging -c conda-forge -c uni-lab -y
mamba install -n check-env --override-channels -c robostack-staging -c conda-forge -c uni-lab conda-forge::uv conda-forge::opencv robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs robostack-staging::ros-humble-nav2-msgs uni-lab::ros-humble-unilabos-msgs robostack-staging::ros-humble-cv-bridge robostack-staging::ros-humble-vision-opencv robostack-staging::ros-humble-tf-transformations robostack-staging::ros-humble-moveit-msgs robostack-staging::ros-humble-tf2-ros robostack-staging::ros-humble-tf2-ros-py conda-forge::transforms3d -y
- name: Install pip dependencies and unilabos
run: |

View File

@@ -1,6 +1,10 @@
name: Build Conda-Pack Environment
on:
# 在 UniLabOS Conda Build 成功上传后自动构建非全量 conda-pack
workflow_run:
workflows: ["UniLabOS Conda Build"]
types: [completed]
workflow_dispatch:
inputs:
branch:
@@ -21,6 +25,16 @@ on:
jobs:
build-conda-pack:
if: |
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'workflow_run'
)
env:
BUILD_FULL: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}
PACKAGE_REF: ${{ github.event.inputs.branch || github.event.workflow_run.head_sha || github.ref_name }}
strategy:
fail-fast: false
matrix:
@@ -29,7 +43,7 @@ jobs:
platform: linux-64
env_file: unilabos-linux-64.yaml
script_ext: sh
- os: macos-15 # Intel (via Rosetta)
- os: macos-15-intel # Intel x86_64
platform: osx-64
env_file: unilabos-osx-64.yaml
script_ext: sh
@@ -54,7 +68,9 @@ jobs:
id: should_build
shell: bash
run: |
if [[ -z "${{ github.event.inputs.platforms }}" ]]; then
if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then
echo "should_build=true" >> $GITHUB_OUTPUT
elif [[ -z "${{ github.event.inputs.platforms }}" ]]; then
echo "should_build=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event.inputs.platforms }}" == *"${{ matrix.platform }}"* ]]; then
echo "should_build=true" >> $GITHUB_OUTPUT
@@ -65,17 +81,17 @@ jobs:
- uses: actions/checkout@v6
if: steps.should_build.outputs.should_build == 'true'
with:
ref: ${{ github.event.inputs.branch }}
ref: ${{ github.event.inputs.branch || github.event.workflow_run.head_sha || github.ref }}
fetch-depth: 0
- name: Setup Miniforge (with mamba)
if: steps.should_build.outputs.should_build == 'true'
uses: conda-incubator/setup-miniconda@v3
uses: conda-incubator/setup-miniconda@v4
with:
miniforge-version: latest
use-mamba: true
python-version: '3.11.14'
channels: conda-forge,robostack-staging,uni-lab,defaults
channels: conda-forge,robostack-staging,uni-lab
channel-priority: flexible
activate-environment: unilab
auto-update-conda: false
@@ -86,13 +102,13 @@ jobs:
run: |
echo Installing unilabos and dependencies to unilab environment...
echo Using mamba for faster and more reliable dependency resolution...
echo Build full: ${{ github.event.inputs.build_full }}
if "${{ github.event.inputs.build_full }}"=="true" (
echo Build full: ${{ env.BUILD_FULL }}
if "${{ env.BUILD_FULL }}"=="true" (
echo Installing unilabos-full ^(complete package^)...
mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos-full conda-pack zstandard -y
) else (
echo Installing unilabos ^(minimal package^)...
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos conda-pack zstandard -y
)
- name: Install conda-pack, unilabos and dependencies (Unix)
@@ -101,13 +117,13 @@ jobs:
run: |
echo "Installing unilabos and dependencies to unilab environment..."
echo "Using mamba for faster and more reliable dependency resolution..."
echo "Build full: ${{ github.event.inputs.build_full }}"
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
echo "Build full: ${{ env.BUILD_FULL }}"
if [[ "${{ env.BUILD_FULL }}" == "true" ]]; then
echo "Installing unilabos-full (complete package)..."
mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos-full conda-pack zstandard -y
else
echo "Installing unilabos (minimal package)..."
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos conda-pack zstandard -y
fi
- name: Get latest ros-humble-unilabos-msgs version (Windows)
@@ -134,27 +150,27 @@ jobs:
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
run: |
echo Checking for available ros-humble-unilabos-msgs versions...
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo Search completed
mamba search --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs || echo Search completed
echo.
echo Updating ros-humble-unilabos-msgs to latest version...
mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo Already at latest version
mamba update -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs -y || echo Already at latest version
- name: Check for newer ros-humble-unilabos-msgs (Unix)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
shell: bash
run: |
echo "Checking for available ros-humble-unilabos-msgs versions..."
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo "Search completed"
mamba search --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs || echo "Search completed"
echo ""
echo "Updating ros-humble-unilabos-msgs to latest version..."
mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo "Already at latest version"
mamba update -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs -y || echo "Already at latest version"
- name: Install latest unilabos from source (Windows)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
run: |
echo Uninstalling existing unilabos...
mamba run -n unilab pip uninstall unilabos -y || echo unilabos not installed via pip
echo Installing unilabos from source (branch: ${{ github.event.inputs.branch }})...
echo Installing unilabos from source (ref: ${{ env.PACKAGE_REF }})...
mamba run -n unilab pip install .
echo Verifying installation...
mamba run -n unilab pip show unilabos
@@ -165,7 +181,7 @@ jobs:
run: |
echo "Uninstalling existing unilabos..."
mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip"
echo "Installing unilabos from source (branch: ${{ github.event.inputs.branch }})..."
echo "Installing unilabos from source (ref: ${{ env.PACKAGE_REF }})..."
mamba run -n unilab pip install .
echo "Verifying installation..."
mamba run -n unilab pip show unilabos
@@ -226,7 +242,9 @@ jobs:
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
run: |
echo Packing unilab environment with conda-pack...
mamba activate unilab && conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
for /f "delims=" %%i in ('mamba run -n unilab python -c "import os; print(os.environ['CONDA_PREFIX'])"') do set "UNILAB_PREFIX=%%i"
echo Packing environment at: %UNILAB_PREFIX%
mamba run -n unilab conda-pack -p "%UNILAB_PREFIX%" -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
echo Pack file created:
dir unilab-env-${{ matrix.platform }}.tar.gz
@@ -235,8 +253,9 @@ jobs:
shell: bash
run: |
echo "Packing unilab environment with conda-pack..."
mamba install conda-pack -c conda-forge -y
conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
UNILAB_PREFIX="$(mamba run -n unilab python -c 'import os; print(os.environ["CONDA_PREFIX"])')"
echo "Packing environment at: $UNILAB_PREFIX"
mamba run -n unilab conda-pack -p "$UNILAB_PREFIX" -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
echo "Pack file created:"
ls -lh unilab-env-${{ matrix.platform }}.tar.gz
@@ -267,7 +286,7 @@ jobs:
rem Create README using Python script
echo Creating: README.txt
python scripts\create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package\README.txt
python scripts\create_readme.py ${{ matrix.platform }} ${{ env.PACKAGE_REF }} dist-package\README.txt
echo.
echo Distribution package contents:
@@ -303,7 +322,7 @@ jobs:
# Create README using Python script
echo "Creating: README.txt"
python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/README.txt
python scripts/create_readme.py ${{ matrix.platform }} ${{ env.PACKAGE_REF }} dist-package/README.txt
echo ""
echo "Distribution package contents:"
@@ -314,7 +333,7 @@ jobs:
if: steps.should_build.outputs.should_build == 'true'
uses: actions/upload-artifact@v6
with:
name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }}
path: dist-package/
retention-days: 90
if-no-files-found: error
@@ -326,9 +345,9 @@ jobs:
echo Build Summary
echo ==========================================
echo Platform: ${{ matrix.platform }}
echo Branch: ${{ github.event.inputs.branch }}
echo Branch: ${{ env.PACKAGE_REF }}
echo Python version: 3.11.14
if "${{ github.event.inputs.build_full }}"=="true" (
if "${{ env.BUILD_FULL }}"=="true" (
echo Package: unilabos-full ^(complete^)
) else (
echo Package: unilabos ^(minimal^)
@@ -337,7 +356,7 @@ jobs:
echo Distribution package contents:
dir dist-package
echo.
echo Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
echo Artifact name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }}
echo.
echo After download, extract the ZIP and run:
echo install_unilab.bat
@@ -351,9 +370,9 @@ jobs:
echo "Build Summary"
echo "=========================================="
echo "Platform: ${{ matrix.platform }}"
echo "Branch: ${{ github.event.inputs.branch }}"
echo "Branch: ${{ env.PACKAGE_REF }}"
echo "Python version: 3.11.14"
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
if [[ "${{ env.BUILD_FULL }}" == "true" ]]; then
echo "Package: unilabos-full (complete)"
else
echo "Package: unilabos (minimal)"
@@ -362,7 +381,7 @@ jobs:
echo "Distribution package contents:"
ls -lh dist-package/
echo ""
echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}"
echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }}"
echo ""
echo "After download:"
echo " install_unilab.sh"

View File

@@ -51,12 +51,12 @@ jobs:
fetch-depth: 0
- name: Setup Miniforge (with mamba)
uses: conda-incubator/setup-miniconda@v3
uses: conda-incubator/setup-miniconda@v4
with:
miniforge-version: latest
use-mamba: true
python-version: '3.11.14'
channels: conda-forge,robostack-staging,uni-lab,defaults
channels: conda-forge,robostack-staging,uni-lab
channel-priority: flexible
activate-environment: unilab
auto-update-conda: false
@@ -66,7 +66,7 @@ jobs:
run: |
echo "Installing unilabos and dependencies to unilab environment..."
echo "Using mamba for faster and more reliable dependency resolution..."
mamba install -n unilab uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos -y
- name: Install latest unilabos from source
run: |
@@ -84,7 +84,7 @@ jobs:
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
if: |
github.event.workflow_run.head_branch == 'main' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
@@ -105,7 +105,7 @@ jobs:
test -f docs/_build/html/index.html && echo "✓ index.html exists" || echo "✗ index.html missing"
- name: Upload build artifacts
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
if: |
github.event.workflow_run.head_branch == 'main' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
@@ -125,4 +125,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5

View File

@@ -10,6 +10,9 @@ on:
# 支持 tag 推送(不依赖 CI Check
push:
tags: ['v*']
# GitHub Release 发布时自动构建并上传
release:
types: [published]
# 手动触发
workflow_dispatch:
inputs:
@@ -60,7 +63,7 @@ jobs:
- os: ubuntu-latest
platform: linux-64
env_file: unilabos-linux-64.yaml
- os: macos-15 # Intel (via Rosetta)
- os: macos-15-intel # Intel x86_64
platform: osx-64
env_file: unilabos-osx-64.yaml
- os: macos-latest # ARM64
@@ -80,7 +83,7 @@ jobs:
- uses: actions/checkout@v6
with:
# 如果是 workflow_run 触发,使用触发 CI Check 的 commit
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
ref: ${{ github.event.workflow_run.head_sha || github.event.release.tag_name || github.ref }}
fetch-depth: 0
- name: Check if platform should be built
@@ -96,12 +99,14 @@ jobs:
echo "should_build=false" >> $GITHUB_OUTPUT
fi
- name: Setup Miniconda
- name: Setup Miniforge
if: steps.should_build.outputs.should_build == 'true'
uses: conda-incubator/setup-miniconda@v3
uses: conda-incubator/setup-miniconda@v4
with:
miniconda-version: 'latest'
channels: conda-forge,robostack-staging,defaults
miniforge-version: latest
use-mamba: true
python-version: '3.11.14'
channels: conda-forge,robostack-staging
channel-priority: strict
activate-environment: build-env
auto-update-conda: false
@@ -110,24 +115,22 @@ jobs:
- name: Install rattler-build and anaconda-client
if: steps.should_build.outputs.should_build == 'true'
run: |
conda install -c conda-forge rattler-build anaconda-client
mamba install -n build-env --override-channels -c conda-forge rattler-build anaconda-client -y
- name: Show environment info
if: steps.should_build.outputs.should_build == 'true'
run: |
conda info
conda list | grep -E "(rattler-build|anaconda-client)"
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
conda run -n build-env rattler-build --version
conda run -n build-env anaconda --version
echo "Platform: ${{ matrix.platform }}"
echo "OS: ${{ matrix.os }}"
- name: Build conda package
if: steps.should_build.outputs.should_build == 'true'
run: |
if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
else
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
fi
conda run -n build-env rattler-build build -r ./recipes/msgs/recipe.yaml --target-platform ${{ matrix.platform }} -c robostack -c robostack-staging -c conda-forge
- name: List built packages
if: steps.should_build.outputs.should_build == 'true'
@@ -157,9 +160,15 @@ jobs:
retention-days: 30
- name: Upload to Anaconda.org (unilab organization)
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
if: |
steps.should_build.outputs.should_build == 'true' &&
(
github.event_name == 'release' ||
startsWith(github.ref, 'refs/tags/') ||
github.event.inputs.upload_to_anaconda == 'true'
)
run: |
for package in $(find ./output -name "*.conda"); do
echo "Uploading $package to unilab organization..."
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
done

View File

@@ -1,14 +1,10 @@
name: UniLabOS Conda Build
on:
# 在 CI Check 成功后自动触发
# 在 Multi-Platform Conda Build 成功上传 msgs 后自动触发
workflow_run:
workflows: ["CI Check"]
workflows: ["Multi-Platform Conda Build"]
types: [completed]
branches: [main, dev]
# 标签推送时直接触发(发布版本)
push:
tags: ['v*']
# 手动触发
workflow_dispatch:
inputs:
@@ -33,37 +29,37 @@ on:
type: boolean
jobs:
# 等待 CI Check 完成的 job (仅用于 workflow_run 触发)
wait-for-ci:
# 等待上游 msgs 构建完成的 job (仅用于 workflow_run 触发)
wait-for-upstream:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_run'
outputs:
should_continue: ${{ steps.check.outputs.should_continue }}
steps:
- name: Check CI status
- name: Check upstream workflow status
id: check
run: |
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" && ( "${{ github.event.workflow_run.event }}" == "release" || "${{ github.event.workflow_run.event }}" == "push" ) ]]; then
echo "should_continue=true" >> $GITHUB_OUTPUT
echo "CI Check passed, proceeding with build"
echo "Multi-Platform Conda Build passed for release/tag, proceeding with UniLabOS build"
else
echo "should_continue=false" >> $GITHUB_OUTPUT
echo "CI Check did not succeed (status: ${{ github.event.workflow_run.conclusion }}), skipping build"
echo "Upstream workflow is not a successful release/tag build (status: ${{ github.event.workflow_run.conclusion }}, event: ${{ github.event.workflow_run.event }}), skipping build"
fi
build:
needs: [wait-for-ci]
# 运行条件workflow_run 触发且 CI 成功,或者其他触发方式
needs: [wait-for-upstream]
# 运行条件workflow_run 触发且上游成功,或者手动触发
if: |
always() &&
(needs.wait-for-ci.result == 'skipped' || needs.wait-for-ci.outputs.should_continue == 'true')
(needs.wait-for-upstream.result == 'skipped' || needs.wait-for-upstream.outputs.should_continue == 'true')
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform: linux-64
- os: macos-15 # Intel (via Rosetta)
- os: macos-15-intel # Intel x86_64
platform: osx-64
- os: macos-latest # ARM64
platform: osx-arm64
@@ -79,7 +75,7 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
# 如果是 workflow_run 触发,使用触发 CI Check 的 commit
# 如果是 workflow_run 触发,使用上游 conda 包构建的 commit
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
fetch-depth: 0
@@ -96,12 +92,14 @@ jobs:
echo "should_build=false" >> $GITHUB_OUTPUT
fi
- name: Setup Miniconda
- name: Setup Miniforge
if: steps.should_build.outputs.should_build == 'true'
uses: conda-incubator/setup-miniconda@v3
uses: conda-incubator/setup-miniconda@v4
with:
miniconda-version: 'latest'
channels: conda-forge,robostack-staging,uni-lab,defaults
miniforge-version: latest
use-mamba: true
python-version: '3.11.14'
channels: conda-forge,robostack-staging,uni-lab
channel-priority: strict
activate-environment: build-env
auto-update-conda: false
@@ -110,20 +108,22 @@ jobs:
- name: Install rattler-build and anaconda-client
if: steps.should_build.outputs.should_build == 'true'
run: |
conda install -c conda-forge rattler-build anaconda-client
mamba install -n build-env --override-channels -c conda-forge rattler-build anaconda-client -y
- name: Show environment info
if: steps.should_build.outputs.should_build == 'true'
run: |
conda info
conda list | grep -E "(rattler-build|anaconda-client)"
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
conda run -n build-env rattler-build --version
conda run -n build-env anaconda --version
echo "Platform: ${{ matrix.platform }}"
echo "OS: ${{ matrix.os }}"
echo "Build full package: ${{ github.event.inputs.build_full || 'false' }}"
echo "Build full package: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}"
echo "Building packages:"
echo " - unilabos-env (environment dependencies)"
echo " - unilabos (with pip package)"
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
if [[ "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}" == "true" ]]; then
echo " - unilabos-full (complete package)"
fi
@@ -131,14 +131,19 @@ jobs:
if: steps.should_build.outputs.should_build == 'true'
run: |
echo "Building unilabos-env (conda environment dependencies)..."
rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
conda run -n build-env rattler-build build -r .conda/environment/recipe.yaml --target-platform ${{ matrix.platform }} -c uni-lab -c robostack-staging -c conda-forge
- name: Upload unilabos-env to Anaconda.org (if enabled)
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
if: |
steps.should_build.outputs.should_build == 'true' &&
(
github.event_name == 'workflow_run' ||
github.event.inputs.upload_to_anaconda == 'true'
)
run: |
echo "Uploading unilabos-env to uni-lab organization..."
for package in $(find ./output -name "unilabos-env*.conda"); do
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
done
- name: Build unilabos (with pip package)
@@ -146,33 +151,40 @@ jobs:
run: |
echo "Building unilabos package..."
# 如果已上传到 Anaconda从 uni-lab channel 获取 unilabos-env否则从本地 output 获取
rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
conda run -n build-env rattler-build build -r .conda/base/recipe.yaml --target-platform ${{ matrix.platform }} -c uni-lab -c robostack-staging -c conda-forge --channel ./output
- name: Upload unilabos to Anaconda.org (if enabled)
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
if: |
steps.should_build.outputs.should_build == 'true' &&
(
github.event_name == 'workflow_run' ||
github.event.inputs.upload_to_anaconda == 'true'
)
run: |
echo "Uploading unilabos to uni-lab organization..."
for package in $(find ./output -name "unilabos-0*.conda" -o -name "unilabos-[0-9]*.conda"); do
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
done
- name: Build unilabos-full - Only when explicitly requested
if: |
steps.should_build.outputs.should_build == 'true' &&
github.event_name == 'workflow_dispatch' &&
github.event.inputs.build_full == 'true'
run: |
echo "Building unilabos-full package on ${{ matrix.platform }}..."
rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
conda run -n build-env rattler-build build -r .conda/full/recipe.yaml --target-platform ${{ matrix.platform }} -c uni-lab -c robostack-staging -c conda-forge --channel ./output
- name: Upload unilabos-full to Anaconda.org (if enabled)
if: |
steps.should_build.outputs.should_build == 'true' &&
github.event_name == 'workflow_dispatch' &&
github.event.inputs.build_full == 'true' &&
github.event.inputs.upload_to_anaconda == 'true'
run: |
echo "Uploading unilabos-full to uni-lab organization..."
for package in $(find ./output -name "unilabos-full*.conda"); do
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
done
- name: List built packages

5
.gitignore vendored
View File

@@ -251,7 +251,4 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
*.bz2
test_config.py
# Local config files with secrets
yibin_coin_cell_only_config.json
yibin_electrolyte_config.json
yibin_electrolyte_only_config.json

View File

@@ -1,72 +0,0 @@
# CSV 导出功能变更概要
## 修改的文件
### 1. [bioyond_cell_workstation.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py)
#### 新增导入
- `import csv``import os`L14-15
#### 新增方法
| 方法 | 功能 |
|------|------|
| `_extract_prep_bottle_from_report` | 从 order_finish 报文提取**配液瓶**信息每订单最多1个 |
| `_extract_vial_bottles_from_report` | 从 order_finish 报文提取**分液瓶**信息(每订单可多个,返回数组) |
| `_export_order_csv` | 汇总所有信息写入 CSV 文件 |
#### 配液瓶筛选逻辑 (`_extract_prep_bottle_from_report`)
- `typemode="1"`, `realQuantity=1`, `usedQuantity=1`
- `locationId``3a19deae-2c7a-` 开头(手动传递窗)
- LIMS API 二次确认:`typeName` 含"配液瓶(小)"或"配液瓶(大)"
#### 分液瓶筛选逻辑 (`_extract_vial_bottles_from_report`)
- `typemode="1"`, `realQuantity=1`, `usedQuantity=1`
- `locationId``3a19debc-84b5-``3a19debe-5200` 开头(自动堆栈-左/右)
- LIMS API 二次确认:`typeName` 为"5ml分液瓶"或"20ml分液瓶"
- **返回数组**,支持 1×5ml + n×20ml 的组合
#### 修改的方法
| 方法 | 变更 |
|------|------|
| `_submit_and_wait_orders` | 新增配液瓶+分液瓶提取步骤,将 `prep_bottles``vial_bottles` 存入 `final_result` |
| `create_orders` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` |
| `create_orders_formulation` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` |
#### CSV 输出格式
```
orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间
```
- 单个分液瓶时直接写值;多个分液瓶时类型和二维码用 JSON 数组表示
- CSV 编码使用 `utf-8-sig`(兼容 Excel 打开)
- `csv_export_path` 默认为空字符串,不传则不导出(向后兼容)
---
### 2. [bioyond_cell.yaml](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/registry/devices/bioyond_cell.yaml)
为两个 action 注册了 `csv_export_path` 参数:
- `auto-create_orders`: `goal_default` + `schema.properties.goal.properties` 中添加 `csv_export_path`
- `auto-create_orders_formulation`: 同上
---
### 3. [coin_cell_assembly.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py) 的 CSV 改动与全流程追溯
`bioyond_cell_workstation.py``_submit_and_wait_orders` 最后阶段,提取 `prep_bottles`(配液瓶)和 `vial_bottles`(分液瓶)的条码并随 `mass_ratios` 数组一起下发给各下游工站(例如扣电组装站),实现跨站的全流程配方追溯。
并在扣电站生成的 `date_xxx.csv` 中,**替换并新增**了以下列:
- 移除了原有的 `formulation_order_code` 与合并的 `formulation_ratio` 列。
- 新增 `orderName` 导出
- 新增 `prep_bottle_barcode`(奔曜传递的配液瓶二维码)
- 新增 `vial_bottle_barcodes`(奔曜传递的分液瓶二维码,多瓶时存 JSON 数组)
- 新增 `target_mass_ratio` 理论目标质量比
- 新增 `real_mass_ratio` 实际称量真实质量比
*注意:这与操作人员在手套箱内扫码传入扣电站的 `electrolyte_code` 是单独记录的,方便做数据核对。*
## 向后兼容性
- `csv_export_path` 默认值为 `""`(空字符串),现有调用不受影响
- 新增的 `prep_bottles``vial_bottles` 字段为 `final_result``mass_ratios` 内部的新增附属字段,不破坏现有数据结构。

View File

@@ -1,168 +0,0 @@
# 变更说明 2026-03-24
## 问题背景
`BioyondElectrolyteDeck`(原 `BIOYOND_YB_Deck`)迁移后,前端物料未能正常上传/同步。
---
## 修复内容
### 1. `unilabos/resources/bioyond/decks.py`
- 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑,与旧版 `BIOYOND_YB_Deck` 保持一致
- 工厂函数 `bioyond_electrolyte_deck` 保留显式调用 `deck.setup()`,避免重复初始化
```python
# 修复前(缺少 setup 参数,无法通过 setup=True 触发初始化)
def __init__(self, name, size_x, size_y, size_z, category):
super().__init__(...)
# 修复后
def __init__(self, name, size_x, size_y, size_z, category, setup: bool = False):
super().__init__(...)
if setup:
self.setup()
```
---
### 2. `unilabos/resources/graphio.py`
- 修复 `resource_bioyond_to_plr` 中两处 `bottle.tracker.liquids` 直接赋值导致的崩溃
- `ResourceHolder`(如枪头盒的 TipSpot 槽位)没有 `tracker` 属性,直接访问会抛出 `AttributeError`,阻断整个 Bioyond 同步流程
```python
# 修复前
bottle.tracker.liquids = [...]
# 修复后
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
bottle.tracker.liquids = [...]
```
---
### 3. `unilabos/app/main.py`
- 保留 `file_path is not None` 条件不变(已还原),并补充注释说明原因
- 该逻辑只在**本地文件模式**下有意义:本地 graph 文件只含设备结构远端有已保存物料merge 才能将两者合并
- 远端模式(`file_path=None`)下,`resource_tree_set``request_startup_json` 来自同一份数据merge 为空操作,条件是否加 `file_path is not None` 对结果没有影响
---
### 4. `unilabos/devices/workstation/bioyond_studio/station.py` ⭐ 核心修复
- 当 deck 通过反序列化创建时,不会自动调用 `setup()`,导致 `deck.children` 为空,`warehouses` 始终是 `{}`
- 增加兜底逻辑:仓库扫描后仍为空,则主动调用 `deck.setup()` 初始化仓库
- 这是导致所有物料放置失败(`warehouse '...' 在deck中不存在。可用warehouses: []`)的根本原因
```python
# 新增兜底
if not self.deck.warehouses and hasattr(self.deck, "setup") and callable(self.deck.setup):
logger.info("Deck 无仓库子节点,调用 setup() 初始化仓库")
self.deck.setup()
```
---
---
## 补充修复 2026-03-25依华扣电组装工站子物料未上传
### 问题
`CoinCellAssemblyWorkstation.post_init` 直接上传空 deck未调用 `deck.setup()`,导致:
- 前端子物料(成品弹夹、料盘、瓶架等)不显示
- 运行时 `self.deck.get_resource("成品弹夹")` 抛出 `ResourceNotFoundError`
### 修复文件
**`unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`**
- `YihuaCoinCellDeck.__init__` 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑
**`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`**
- `post_init` 中增加与 Bioyond 工站相同的兜底逻辑deck 无子节点时调用 `deck.setup()` 初始化
```python
# post_init 中新增
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
logger.info("YihuaCoinCellDeck 无子节点,调用 setup() 初始化")
self.deck.setup()
```
### 联动 Bug`MaterialPlate.create_with_holes` 构造顺序错误
**现象**`deck.setup()` 被调用后,启动时抛出:
```
设备后初始化失败: Must specify either `ordered_items` or `ordering`.
```
**根因**`create_with_holes` 原来的逻辑是先构造空的 `MaterialPlate` 实例,再 assign 洞位:
```python
# 旧错误cls(...) 时 ordered_items=None → ItemizedResource.__init__ 立即报错
plate = cls(name=name, ...) # ← 这里就崩了
holes = create_ordered_items_2d(...) # ← 根本没走到这里
for hole_name, hole in holes.items():
plate.assign_child_resource(...)
```
pylabrobot 的 `ItemizedResource.__init__` 强制要求 `ordered_items``ordering` 必须有一个不为 `None`,空构造直接失败。
**修复**:先建洞位,再作为 `ordered_items` 传给构造函数:
```python
# 新(正确):先建洞位,再一次性传入构造函数
holes = create_ordered_items_2d(klass=MaterialHole, num_items_x=4, ...)
return cls(name=name, ..., ordered_items=holes)
```
> 此 bug 此前未被触发,是因为 `deck.setup()` 从未被调用到——正是上面 `post_init` 兜底修复引出的联动问题。
---
## 补充修复 2026-03-253→2→1 转运资源同步失败
### 问题
配液工站Bioyond完成分液后调用 `transfer_3_to_2_to_1_auto` 将分液瓶板转运到扣电工站BatteryStation。物理 LIMS 转运成功,但数字孪生资源树同步始终失败:
```
[资源同步] ❌ 失败: 目标设备 'BatteryStation' 中未找到资源 'bottle_rack_6x2'
```
### 根因
`_get_resource_from_device` 方法负责跨设备查找资源对象,有两个问题:
1. **原始路径完全失效**:尝试 `from unilabos.app.ros2_app import get_device_plr_resource_by_name`,但该模块不存在,`ImportError``except Exception: pass` 静默吞掉
2. **降级路径搜错地方**:遍历 `self._plr_resources`Bioyond 自己的资源),不可能找到 BatteryStation 的 `bottle_rack_6x2`
### 修复文件
**`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`**
改用全局设备注册表 `registered_devices` 跨设备访问目标 deck
```python
# 修复前(失效)
from unilabos.app.ros2_app import get_device_plr_resource_by_name # 模块不存在
return get_device_plr_resource_by_name(device_id, resource_name)
# 修复后
from unilabos.ros.nodes.base_device_node import registered_devices
device_info = registered_devices.get(device_id)
if device_info is not None:
driver = device_info.get("driver_instance") # TypedDict 是 dict必须用 .get()
if driver is not None:
deck = getattr(driver, "deck", None)
if deck is not None:
res = deck.get_resource(resource_name)
```
关键细节:`DeviceInfoType``TypedDict`(即普通 `dict`),必须用 `device_info.get("driver_instance")` 而非 `getattr(device_info, "driver_instance", None)`——后者对字典永远返回 `None`
---
## 根本原因分析
旧版以**本地文件模式**启动(有 `graph` 文件deck 在启动前已通过 `merge_remote_resources` 获得仓库子节点,反序列化时能正确恢复 warehouses。
新版以**远端模式**启动(`file_path=None`deck 反序列化时没有仓库子节点,`station.py` 扫描为空,所有物料的 warehouse 匹配失败Bioyond 同步的 16 个资源全部无法放置到对应仓库位,前端不显示。

View File

@@ -0,0 +1,611 @@
# PLC 通信标准与设备驱动编写指南(基于 AI4M 工站)
> 本文档以 `unilabos/devices/workstation/AI4M`(水凝胶检测工站)为参考实现,
> 介绍如何将 PLC 控制的实验设备接入 Uni-Lab-OS包含通信协议选型、节点表标准、
> 通信基类、设备驱动、Registry 配置以及调试方法。
>
> 阅读对象:负责现场调试与设备接入的同学。
---
## 0. 总览:一台 PLC 设备从硬件到云端的链路
```
PLC西门子 / 倍福 / 三菱 / 汇川 / 国产 PLC ...
│ 各家 PLC 私有协议S7 / Modbus / EtherCAT ...
┌──────────┴──────────┐
│ OPC UA Server │ ← 统一在 PLC 侧或独立网关上配置
│ (内置或 KEPServer
└──────────┬──────────┘
│ OPC UA over TCP标准协议
┌──────────┴──────────┐
│ Uni-Lab 设备驱动 │ ← 本教程主体
│ AI4MDevice │
│ ├─ base_opcua_client.py 通信基类
│ ├─ opcua_nodes_*.csv 节点表(标准)
│ └─ AI4M.py 动作函数
└──────────┬──────────┘
│ ROS2 Action / 云端 HTTP
实验记录本 / 云端调度
```
**统一约定**:所有 PLC 设备**只暴露 OPC UA 接口**给 Uni-LabPC 端不直接处理 S7 / Modbus 等底层协议。
这是 Uni-Lab 在工站类设备上的 PLC 通信标准。
---
## 1. 为什么选 OPC UA 作为标准?
| 维度 | 自研 TCP/串口协议 | Modbus | **OPC UA** |
|---|---|---|---|
| 厂家无关 | ✗ | 部分 | **✓** |
| 自带类型系统 | ✗ | ✗(裸寄存器) | **Boolean/Int16/Float...** |
| 命名空间 / 节点树 | ✗ | ✗(地址=魔数) | **✓(带名字、可分组)** |
| 订阅推送 | ✗ | ✗ | **DataChange Notification** |
| 鉴权 / 加密 | 自己造 | ✗ | **✓** |
| 与 PLC 工程师沟通成本 | 高 | 中 | **低(按变量名沟通)** |
实际接入时PLC 工程师只需要在 PLC 侧把约定的"上位通讯变量"暴露到 OPC UA Server
我们在 PC 侧就能用 `节点名 + 数据类型` 直接读写,不用管底层是 S7 还是 Modbus。
---
## 2. 节点表标准:`opcua_nodes_xxx.csv`
PLC 侧暴露的所有变量统一**用一张 CSV 表**描述,这是 PC 端和 PLC 端**唯一的接口契约**。
位置示例:`unilabos/devices/workstation/AI4M/opcua_nodes_AI4M.csv`
### 2.1 列定义
| 列名 | 是否必填 | 说明 |
|---|---|---|
| `Name` | ✅ | 节点名PLC 工程师在 PLC 项目中真实使用的变量名,通常是中文/原始名) |
| `EnglishName` | 推荐 | 英文别名,**PC 端代码全部用这个名字**调用 |
| `NodeType` | ✅ | `VARIABLE`(变量)或 `METHOD`方法AI4M 全部用变量 |
| `DataType` | ✅ | `BOOLEAN` / `INT16` / `INT32` / `FLOAT` / `DOUBLE` / `STRING` ... |
| `NodeLanguage` | 推荐 | `Chinese` / `English`,配合 `EnglishName` 做映射 |
| `NodeId` | ✅ | OPC UA 标准 NodeId格式 `ns=<namespace>;s=<string>``ns=<n>;i=<int>` |
### 2.2 真实样例(节选自 `opcua_nodes_AI4M.csv`
| Name | EnglishName | NodeType | DataType | NodeLanguage | NodeId |
|---|---|---|---|---|---|
| 机器人空闲 | `robot_ready` | VARIABLE | BOOLEAN | Chinese | `ns=4;s=上位通讯变量\|机器人空闲` |
| 机器人取烧杯编号 | `robot_pick_beaker_id` | VARIABLE | INT16 | Chinese | `ns=4;s=上位通讯变量\|机器人取烧杯编号` |
| 检测1请求参数 | `station_1_request_params` | VARIABLE | BOOLEAN | Chinese | `ns=4;s=上位通讯变量\|检测1请求参数` |
| 检测1工艺完成 | `station_1_process_complete` | VARIABLE | BOOLEAN | Chinese | `ns=4;s=上位通讯变量\|检测1工艺完成` |
| 磁力搅拌参数设置_C[0].搅拌速度 | `mag_stirrer_c0_stir_speed` | VARIABLE | INT16 | Chinese | `ns=4;s=上位通讯变量\|磁力搅拌参数设置_C[0].搅拌速度` |
| 报警复位 | `alarm_reset` | VARIABLE | BOOLEAN | Chinese | `ns=4;s=上位通讯变量\|报警复位` |
### 2.3 设计规范(必读)
1. **命名按"角色-编号-属性"分层**,便于代码批量寻址:
- `mag_stirrer_c{0..4}_stir_speed`(搅拌仪 0~4 的搅拌速度)
- `station_{1..3}_process_complete`(检测站 1~3 的完成信号)
- `robot_rack_pick_beaker_{1..5}_complete`(取烧杯 1~5 的完成信号)
这样在驱动里可以直接 `f"mag_stirrer_c{idx}_stir_speed"` 拼出节点名。
2. **数据类型与 PLC 侧严格一致**
- `BOOL``BOOLEAN``INT/WORD``INT16/UINT16``DINT``INT32``REAL``FLOAT`
- 类型不一致会触发 `BadTypeMismatch`,写入失败。
3. **NodeId 必须从 PLC 工程或 OPC UA Server 中导出**,不要自己拼。
常见格式:
- 西门子 1500`ns=4;s=上位通讯变量|<变量名>`
- 倍福 TwinCAT`ns=4;s=PLC1.MAIN.<变量名>`
- KEPServerEX`ns=2;s=Channel1.Device1.<Tag>`
4. **每个工站一个独立 CSV**,不要共用。
AI4M 中真机用 `opcua_nodes_AI4M.csv`,仿真用 `opcua_nodes_AI4M_sim.csv`
---
## 3. 通信基类架构
文件:`unilabos/devices/workstation/AI4M/base_opcua_client.py`
整个通信层分两层:
```
BaseOpcUaClient # 最小可用:连接 + 节点注册 + 读写 + 方法调用
│ 继承
OpcUaClientWithSubscription # 生产可用:+ 订阅推送 + 缓存 + 自动重连
│ 继承
AI4MDevice # 业务驱动:在它之上写设备动作函数
```
### 3.1 `BaseOpcUaClient` 核心能力
```python
class BaseOpcUaClient(UniversalDriver):
client: Optional[Client] = None
_node_registry: Dict[str, OpcUaNodeBase] = {} # name -> Variable/Method
_name_mapping: Dict[str, str] = {} # 英文名 -> 中文名
_reverse_mapping: Dict[str, str] = {} # 中文名 -> 英文名
_found_node_objects: Dict[str, Any] = {} # 缓存 ua.Node 用于订阅
@classmethod
def load_csv(cls, file_path) -> Tuple[List[OpcUaNode], dict, dict]: ...
def register_node_list(self, node_list) -> "BaseOpcUaClient": ...
def use_node(self, name) -> OpcUaNodeBase: ...
def read_node(self, node_name: str) -> str: ... # 返回 JSON
def write_node(self, json_input: str) -> str: ...
def call_method(self, node_name, *args) -> Tuple[Any, bool]: ...
```
它做的事情可以归纳为四步:
1. **`load_csv`**:读取节点表,建立 `Name ↔ EnglishName` 双向映射。
2. **`register_node_list`**:把节点登记进 `_variables_to_find` 待查找列表。
3. **`_connect``_find_nodes`**:连上 OPC UA 后,按 `NodeId` 把每个节点解析成 `Variable` / `Method` 对象,放进 `_node_registry`
4. **`use_node(name)`**:业务代码取节点的唯一入口,**支持中英文混用**,找不到会自动重试一次。
### 3.2 `OpcUaClientWithSubscription` 增强能力
`BaseOpcUaClient` 基础上提供三个生产环境必备的能力:
#### a) 订阅缓存(高频读零开销)
```python
def _setup_subscriptions(self):
self._subscription = self.client.create_subscription(
self._subscription_interval, # 默认 500ms
SubscriptionHandler(self),
)
for node_name, node in self._node_registry.items():
if node.type == NodeType.VARIABLE and node.node_id:
handle = self._subscription.subscribe_data_change(ua_node)
self._subscription_handles[node_name] = handle
```
当 PLC 侧变量变化时,`datachange_notification` 回调会把新值写进 `self._node_values[name]`
后续 `get_node_value` 优先读缓存——**业务代码可以放心地写 `while not self.get_node_value(...): time.sleep(1)` 而不用担心 OPC UA 频繁请求**。
#### b) 智能缓存的 `get_node_value`
```python
def get_node_value(self, name, use_cache=True, force_read=False):
# 1. 中英文名归一化
chinese_name = self._name_mapping.get(name, name)
# 2. force_read=True 强制透传到 OPC UA Server
if force_read: ...
# 3. 命中订阅推送 → 直接返回缓存
# 4. 命中按需读 + 未过期cache_timeout=5s→ 返回缓存
# 5. 否则发起 read 并更新缓存
```
#### c) 连接监控 + 自动重连
后台线程每 30s 调一次 `client.get_namespace_array()` 探活,断线则自动 `disconnect → connect → 重新订阅`,最多重试 5 次。
### 3.3 数据类型 / 节点类型
`unilabos/device_comms/opcua_client/node/uniopcua.py`
```python
class DataType(Enum):
BOOLEAN = VariantType.Boolean
INT16 = VariantType.Int16
INT32 = VariantType.Int32
FLOAT = VariantType.Float
STRING = VariantType.String
# ...
class NodeType(Enum):
VARIABLE = NodeClass.Variable
METHOD = NodeClass.Method
OBJECT = NodeClass.Object
```
`Variable.write()` 内部会按 `DataType` 做强制类型转换,
所以 CSV 里的 `DataType` 列就是"PC 端转换写入值的类型说明书"。
---
## 4. 编写设备驱动:以 `AI4MDevice` 为例
文件:`unilabos/devices/workstation/AI4M/AI4M.py`
### 4.1 继承通信基类,最小骨架
```python
from typing import Optional
from unilabos.devices.workstation.AI4M.base_opcua_client import OpcUaClientWithSubscription
class AI4MDevice(OpcUaClientWithSubscription):
def __init__(
self,
url: str, # opc.tcp://192.168.1.10:4840
deck: Optional[AI4M_deck] = None, # 物料台面(资源树)
csv_path: str = None, # 节点表 CSV
username: str = None,
password: str = None,
use_subscription: bool = True,
cache_timeout: float = 5.0,
subscription_interval: int = 500,
*args, **kwargs,
):
super().__init__(
url=url, username=username, password=password,
use_subscription=use_subscription,
cache_timeout=cache_timeout,
subscription_interval=subscription_interval,
*args, **kwargs,
)
# 物料台面初始化(见教程 4. 物料系统)
self.deck = deck or AI4M_deck(setup=True)
self._robot_lock = threading.Lock()
# 关键:加载节点表
if csv_path:
self.load_nodes_from_csv(csv_path)
```
`load_nodes_from_csv` 会一次性完成:解析 CSV → 注册节点 → 解析 NodeId → 建立订阅,
**之后整个驱动都通过 `self.get_node_value(name)` / `self.set_node_value(name, value)` 操作 PLC**
### 4.2 PLC 通信的核心模式握手协议Handshake
PLC 编程的本质是"扫描周期 + 状态机"PC 端**绝对不能用 fire-and-forget 的方式发指令**。
和 PLC 配合的标准模式是 **"PC 写指令 → PC 等待 PLC 回执 → PC 复位指令"**。
AI4M 中所有 `trigger_*` 函数都遵循以下三种握手范式之一:
#### 范式 A脉冲触发 + 完成信号(最常用)
```python
def trigger_init(self) -> dict:
# ① 复位上一轮残留
self.set_node_value("alarm_reset", True); time.sleep(1.0)
self.set_node_value("alarm_reset", False)
self.set_node_value("manual_auto_switch", False)
# ② 等待 PLC 退出自动模式
while self.get_node_value("auto_mode"):
time.sleep(1.0)
# ③ 发起初始化脉冲True → False
self.set_node_value("initialize", True); time.sleep(1.0)
self.set_node_value("initialize", False)
# ④ 等待 PLC 给出完成信号
while not self.get_node_value("init finished"):
time.sleep(1.0)
return {"message": "设备初始化完成"}
```
要点:
- **"PC 写一个 BOOL 拉高再拉低"** 模拟脉冲PLC 用上升沿触发动作。
- **`get_node_value` 要在 while 循环里轮询**,配合订阅缓存基本无压力。
- **每个动作必须有"开始"和"完成"两个独立的 BOOL 节点**,不能复用。
#### 范式 B参数下发 + 请求/已执行/完成 三步握手(带数据的工艺)
```python
def trigger_station_process(self, station_id: int, mag_stir_speed: int, ...):
request_node = f"station_{station_id}_request_params"
params_received_node = f"station_{station_id}_params_received"
start_node = f"station_{station_id}_start"
complete_node = f"station_{station_id}_process_complete"
# ① PC 复位三个状态位(避免上一轮影响)
self._reset_station_process_flags(station_id)
# ② 等 PLC 主动请求参数PLC 准备好了才接收)
while not self.get_node_value(request_node):
time.sleep(1.0)
# ③ PC 下发参数注意PLC 内部数组是 0-basedPC 暴露给用户是 1-based
station_idx = station_id - 1
self.set_node_value(f"mag_stirrer_c{station_idx}_stir_speed", mag_stir_speed)
self.set_node_value(f"mag_stirrer_c{station_idx}_heat_temp", mag_stir_heat_temp)
self.set_node_value(f"mag_stirrer_c{station_idx}_time_set", mag_stir_time_set)
self.set_node_value(f"syringe_pump_{station_idx}_abs_position_set", syringe_pump_abs_pos)
# ④ PC 通知 PLC "参数已就绪",等 PLC 回复"已执行"
self.set_node_value(start_node, True)
while not self.get_node_value(params_received_node):
time.sleep(1.0)
# ⑤ 等 PLC 完成整个工艺
while not self.get_node_value(complete_node):
time.sleep(5.0)
self.set_node_value(start_node, False) # 复位,方便下一轮
return {"station_id": station_id, "message": "..."}
```
四个状态位的语义:
| 信号 | 方向 | 含义 |
|---|---|---|
| `station_X_request_params` | **PLC → PC** | "我准备好了,把参数给我" |
| `station_X_start` | **PC → PLC** | "参数我已经写好了,开干" |
| `station_X_params_received` | **PLC → PC** | "参数我已经吃下了" |
| `station_X_process_complete` | **PLC → PC** | "工艺已经做完" |
**这是 PLC 通信教科书级别的标准范式**,所有带数据下发的动作都建议照抄。
#### 范式 C编号下发 + 编号对应的完成信号(多目标互锁)
```python
def trigger_robot_pick_beaker(self, pick_beaker_id: int, place_station_id: int = None, ...):
# ① 等机器人空闲(互锁)
while not self.get_node_value("robot_ready"):
time.sleep(1.0)
# ② 阶段一:下发"取哪一杯"编号 + 等"取这一杯完成"
pick_complete_node = f"robot_rack_pick_beaker_{pick_beaker_id}_complete"
self.set_node_value("robot_pick_beaker_id", pick_beaker_id)
while not self.get_node_value(pick_complete_node):
time.sleep(1.0)
# ③ 阶段二:下发"放到哪个工站"编号 + 等"放完成"
place_complete_node = f"robot_place_station_{place_station_id}_complete"
self._reset_station_process_flags(place_station_id)
self.set_node_value("robot_place_station_id", place_station_id)
while not self.get_node_value(place_complete_node):
time.sleep(1.0)
```
要点:
- **同一个动作的多个目标用"编号变量 + 编号对应的完成信号"实现**,不要每个目标都开一个开始位。
- **配合 Python 端 `threading.Lock()` 做软互锁**,避免多个线程争抢机器人。
- **每个阶段有独立的完成信号**,串行等待,不能合并。
### 4.3 一些容易踩坑的细节
1. **节点名映射**
`set_node_value("alarm_reset", True)` 实际写入的是 CSV 中文名 `报警复位`
`get_node_value` 同理。**业务代码全部用 EnglishName**,不要直接用中文。
2. **PLC 数组索引和 PC 不一致**
AI4M 里 PC 暴露 `station_id ∈ {1, 2, 3}`,但 PLC 内部数组是 `C[0..2]`
驱动里要做 `station_idx = station_id - 1`**这种映射只在驱动层做一次**
不要让上层registry / 实验记录本)感知。
3. **订阅模式下 BOOL 节点的边沿同步**
订阅有 ~500ms 延迟。如果你刚 `set_node_value(x, True)` 就立刻 `get_node_value(x)`
读到的可能还是 `False`(订阅还没推回来)。
解决方案:**写完后用 `force_read=True` 透传一次** 或加一段 `time.sleep`
4. **永远不要忘记复位**
`start` 拉 True 后必须有地方拉回 False否则下一轮 PLC 上升沿不触发。
AI4M 在 `_reset_station_process_flags` 中统一做:
```python
def _reset_station_process_flags(self, station_id: int) -> None:
self.set_node_value(f"station_{station_id}_process_complete", False)
self.set_node_value(f"station_{station_id}_start", False)
self.set_node_value(f"station_{station_id}_params_received", False)
```
5. **耗时长的等待 sleep 加大**
工艺等待用 `time.sleep(5.0)`,机器人等待用 `time.sleep(1.0)`,初始化等待 `time.sleep(1.0)`
不要全部用 0.1s 轮询,会把日志刷爆。
---
## 5. 把驱动接到 Uni-LabRegistry + Graph
### 5.1 Registry YAML动作 schema
文件:`unilabos/registry/devices/AI4M_station.yaml`
```yaml
AI4M_station:
category: [AI4M_station]
class:
module: unilabos.devices.workstation.AI4M.AI4M:AI4MDevice # ← 入口类
type: python
action_value_mappings:
auto-trigger_init:
schema:
description: 设备初始化...
properties:
goal: { properties: {}, required: [], type: object }
result:
properties: { message: { type: string } }
required: [message]
type: object
type: object
type: UniLabJsonCommand
auto-trigger_station_process:
always_free: true
schema:
description: 执行检测工艺流程
properties:
goal:
properties:
station_id: { type: integer, description: 检测编号 1-3 }
mag_stir_stir_speed: { type: integer }
mag_stir_heat_temp: { type: integer }
mag_stir_time_set: { type: integer }
syringe_pump_abs_position_set:{ type: integer }
required: [station_id, mag_stir_stir_speed, mag_stir_heat_temp,
mag_stir_time_set, syringe_pump_abs_position_set]
type: object
result: { ... }
type: UniLabJsonCommand
init_param_schema:
config:
type: object
required: [url]
properties:
url: { type: string, description: OPC UA 服务器地址 }
csv_path: { type: string, description: 节点配置 CSV 路径 }
deck: { type: string, description: 资源树配置 }
username: { type: string }
password: { type: string }
use_subscription: { type: boolean, default: true }
cache_timeout: { type: number, default: 5.0 }
subscription_interval: { type: integer, default: 500 }
```
规则总结:
- `class.module` 指向驱动类(`module:ClassName`)。
- `action_value_mappings` 中的 key 形如 `auto-<方法名>`,对应驱动里的同名 Python 方法。
- `schema.goal` 自动转成 ROS2 Action 的 goal 消息,`schema.result` 转 result。
- `init_param_schema.config` 对应 `__init__` 的入参,**所有需要现场改的参数都要列出来**(最重要的就是 `url` 和 `csv_path`)。
- `always_free: true` 表示该动作不占用工站独占锁(多检测站可并发执行)。
### 5.2 Graph JSON实例化
文件:`unilabos/devices/workstation/AI4M/AI4M.json`
```json
{
"nodes": [
{
"id": "AI4M_station",
"name": "AI4M_station",
"type": "device",
"class": "AI4M_station",
"children": ["AI4M_deck"],
"parent": null,
"config": {
"url": "opc.tcp://192.168.1.10:4840",
"csv_path": "opcua_nodes_AI4M.csv",
"deck": {
"data": {
"_resource_child_name": "AI4M_deck",
"_resource_type": "unilabos.devices.workstation.AI4M.decks:AI4M_deck"
}
}
}
},
{
"id": "AI4M_deck",
"type": "deck",
"class": "AI4M_deck",
"parent": "AI4M_station",
"config": { "type": "AI4M_deck" }
}
]
}
```
要点:
- `class` 必须和 Registry YAML 的顶层 key 完全一致(`AI4M_station`)。
- `config` 字段**逐字传给驱动 `__init__`**,所以 Graph JSON = "现场参数表"。
- 多套相同设备时拷贝一份,把 `id` / `url` 改掉即可(参考 `AI4M002_station`)。
### 5.3 启动命令(来自 `start.md`
```cmd
# 真机
python unilabos/app/main.py -g unilabos/devices/workstation/AI4M/AI4M.json `
--ak <ak> --sk <sk> --upload_registry --addr <api_url> --disable_browser
# 仿真KEPServerEX 跑在本机 49320 端口)
python unilabos/app/main.py -g unilabos/devices/workstation/AI4M/AI4Msim.json `
--ak <ak> --sk <sk> --upload_registry --disable_browser
```
`--upload_registry` 会把 `AI4M_station.yaml` 的 schema 上传到云端,
之后实验记录本就能看到所有 `auto-*` 动作。
---
## 6. 调试方法
### 6.1 用 KEPServerEX 仿真 PLC
不带 PLC 的开发机上,可以用 KEPServerEX或 `python-opcua` 自建 server模拟。
AI4M 提供了一份仿真节点表 `opcua_nodes_AI4M_sim.csv`**只改 NodeId 不改语义**
所以驱动代码无需任何改动即可在本机调试。
### 6.2 单独跑驱动(不开 ROS
在驱动文件末尾的 `if __name__ == '__main__':` 段:
```python
if __name__ == '__main__':
A4 = AI4MDevice(
url="opc.tcp://192.168.1.10:4840",
csv_path="opcua_nodes_AI4M.csv",
)
A4.trigger_init()
print("初始化完成")
A4.trigger_robot_pick_beaker(1, 1)
```
**新动作上线前一定要在这里裸跑一遍**,确认握手时序正确,再往上接 ROS。
### 6.3 看日志判断卡在哪
`base_opcua_client.py` 的日志已经覆盖了所有关键节点:
```
✓ 客户端已连接!
✓ 找到变量节点: 'robot_ready', NodeId: ns=4;s=...
✓ 已订阅节点: robot_ready
✓ 节点查找完成:所有 142 个节点均已找到
```
如果看到 `⚠ 以下 N 个节点未找到`**99% 是 CSV 里的 NodeId 写错了**,回去对一下 PLC 工程导出的 NodeId。
### 6.4 检查节点是否能直接读写
```python
# 透传读,绕过订阅缓存
A4.get_node_value("robot_ready", force_read=True)
# 直接读 JSON 形式(适合从 HTTP/调试面板调)
A4.read_node("robot_ready")
# 写
A4.set_node_value("alarm_reset", True)
A4.write_node('{"node_name": "alarm_reset", "value": false}')
```
---
## 7. 接入新 PLC 设备的 Checklist
接到一台新工站时,按下面顺序做就能保证不漏:
- [ ] 1. 让 PLC 工程师把上位通讯变量整理到 OPC UA Server导出 NodeId 清单。
- [ ] 2. 在 `unilabos/devices/workstation/<设备名>/` 下新建目录,复制 `AI4M/base_opcua_client.py` 不动。
- [ ] 3. 整理 `opcua_nodes_<设备名>.csv`6 列填齐,并补上 `EnglishName`。
- [ ] 4. 在该目录写设备驱动 `<设备名>.py`,继承 `OpcUaClientWithSubscription`
- [ ] `__init__` 调用 `super().__init__` + `self.load_nodes_from_csv(csv_path)`。
- [ ] 每个动作函数用范式 A/B/C 写握手协议。
- [ ] 每个动作函数都返回 `dict`,至少含 `message` 字段。
- [ ] 5. 在 `unilabos/registry/devices/` 下新建 `<设备名>_station.yaml`,配置 `init_param_schema` 和 `action_value_mappings`。
- [ ] 6. 在该目录新建 `<设备名>.json`Graph填好 `url` 和 `csv_path`。
- [ ] 7. 用 `if __name__ == '__main__':` 单独跑驱动确认握手 OK。
- [ ] 8. 用 `python unilabos/app/main.py -g <Graph> --upload_registry ...` 上线,到实验记录本下发动作回归。
---
## 8. 参考实现速查
| 关注点 | 在 AI4M 中看哪里 |
|---|---|
| OPC UA 通信基类 | `base_opcua_client.py` |
| 节点定义类型系统 | `unilabos/device_comms/opcua_client/node/uniopcua.py` |
| 节点表 CSV 标准 | `opcua_nodes_AI4M.csv` |
| 设备驱动入口类 | `AI4M.py: AI4MDevice` |
| 握手范式 A脉冲+完成) | `AI4M.py: trigger_init` |
| 握手范式 B请求/参数/完成) | `AI4M.py: trigger_station_process` |
| 握手范式 C编号+完成) | `AI4M.py: trigger_robot_pick_beaker` |
| 自动模式批量参数下发 | `AI4M.py: download_auto_params` |
| Registry schema | `unilabos/registry/devices/AI4M_station.yaml` |
| Graph 实例化 | `AI4M.json` / `AI4Msim.json` |
| 启动命令 | `start.md` |

View File

@@ -1,5 +1,5 @@
channel_sources:
- robostack,robostack-staging,conda-forge,defaults
- robostack,robostack-staging,conda-forge
gazebo:
- '11'

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,15 @@ from typing import Dict, Any, List
import networkx as nx
import yaml
# Windows 中文系统 stdout 默认 GBK无法编码 banner / emoji 日志中的 Unicode 字符
# 强制 stdout/stderr 用 UTF-8避免 print 触发 UnicodeEncodeError 导致进程崩溃
if sys.platform == "win32":
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
except (AttributeError, OSError):
pass
# 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__))
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
@@ -621,8 +630,6 @@ def main():
continue
# 如果从远端获取了物料信息,则与本地物料进行同步
# 仅在本地文件模式下有意义:本地文件只含设备结构,远端有已保存的物料,需要 merge
# 远端模式下 resource_tree_set 与 request_startup_json 来自同一份数据merge 为空操作
if file_path is not None and request_startup_json and "nodes" in request_startup_json:
print_status("开始同步远端物料到本地...", "info")
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])

View File

@@ -59,6 +59,7 @@ class JobAddReq(BaseModel):
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="")
node_id: str = Field(examples=["node_id"], description="node uuid", default="")
notebook_id: str = Field(examples=["notebook_id"], description="notebook uuid", default="")
server_info: dict = Field(
examples=[{"send_timestamp": 1717000000.0}],
description="server info (auto-generated if empty)",

View File

@@ -10,29 +10,170 @@ import shutil
import sys
_PATCH_MARKER = "# UniLabOS DLL Patch"
_PATCH_END_MARKER = "# End UniLabOS DLL Patch"
# 75 = EX_TEMPFAIL: 临时失败、重试即可,避免与业务退出码冲突
_RESTART_EXIT_CODE = 75
def _build_dll_patch(lib_bin: str, preload_pyd: str = "") -> str:
"""生成一段加在目标文件顶部的 DLL 加载补丁源码。
- 始终把 ``lib_bin`` 加入 DLL 搜索路径,并把 handle 挂在模块属性上,
防止 GC 清掉搜索路径(``os.add_dll_directory`` 的句柄被回收时
目录会被移除)。
- 可选地用 ``ctypes.CDLL`` 预加载一个 .pyd把它的依赖 DLL 提前装入
进程内存,作为 ``rclpy._rclpy_pybind11`` 这类首次加载点的兜底。
"""
# 用 repr() 序列化路径Python 解析 repr 的结果会还原成原始字符串,
# 不需要也不能再叠加 raw-string 前缀(叠了反而会让 \\ 变成两个反斜杠)。
lines = [
_PATCH_MARKER,
"import os as _ulab_os",
f"_ulab_p = {lib_bin!r}",
'if hasattr(_ulab_os, "add_dll_directory") and _ulab_os.path.isdir(_ulab_p):',
" try: _UNILAB_DLL_HANDLE = _ulab_os.add_dll_directory(_ulab_p)",
" except Exception: _UNILAB_DLL_HANDLE = None",
]
if preload_pyd:
lines.extend(
[
"import ctypes as _ulab_ctypes",
f"try: _ulab_ctypes.CDLL({preload_pyd!r})",
"except Exception: pass",
]
)
lines.append(_PATCH_END_MARKER)
return "\n".join(lines) + "\n"
def _apply_dll_patch(file_path: str, lib_bin: str, preload_pyd: str = "") -> bool:
"""把 DLL 补丁前置到 ``file_path``。文件不存在或已打过补丁则返回 False。"""
if not os.path.isfile(file_path):
return False
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
if _PATCH_MARKER in content:
return False
shutil.copy2(file_path, file_path + ".bak")
with open(file_path, "w", encoding="utf-8") as f:
f.write(_build_dll_patch(lib_bin, preload_pyd) + content)
return True
def _print_restart_banner(patched_files):
"""打印重启提示并以 EX_TEMPFAIL 退出。
- 不使用 ANSI 颜色码Windows 旧版 cmd / PowerShell 5 默认不开 VT 处理,
会把 ``\\033[1;33m`` 当做字面字符显示,反而让用户看不到正文。
- 同时写入 stderr 与 stdout某些上层 launcher / supervisor 只重定向
其中一路,写两遍能保证用户至少看到一份。
- 写入前防御性把流切到 UTF-8 with replace``main.py`` 里已经做过一次,
但本模块也可能被绕过 ``main.py`` 的代码路径直接 importreconfigure
失败也只是退回 errors=replace不影响整体流程。
"""
if sys.platform == "win32":
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
except (AttributeError, OSError):
pass
bar = "#" * 78
files_lines = [f"[UniLabOS] - {p}" for p in patched_files]
body = "\n".join(
[
"",
bar,
bar,
"##",
"## [UniLabOS] Windows + conda 下检测到 DLL 加载失败,已自动打补丁。",
"## [UniLabOS] DLL load failure detected on Windows + conda;",
"## [UniLabOS] the following files have been auto-patched:",
"##",
*[f"## {line}" for line in files_lines],
"##",
"## [UniLabOS] 当前进程的 rclpy 状态已损坏,补丁需要在新进程才生效。",
"## [UniLabOS] The current process is unusable; the patch only takes",
"## [UniLabOS] effect on a fresh process.",
"##",
"## >>> 请重新运行刚才的命令 / Please re-run the same command. <<<",
"##",
bar,
bar,
"",
]
)
for stream in (sys.stderr, sys.stdout):
try:
stream.write(body)
stream.flush()
except Exception:
try:
print(body, file=stream)
except Exception:
pass
sys.exit(_RESTART_EXIT_CODE)
def patch_rclpy_dll_windows():
"""在 Windows + conda 环境下 rclpy 打 DLL 加载补丁"""
"""在 Windows + conda 环境下修复 rclpy / rosidl typesupport 的 DLL 加载。
背景conda 安装的 ros 系列包,其原生扩展依赖 ``$CONDA_PREFIX/Library/bin``
下的 DLL只有 conda 环境被正确激活、且 PATH 中含 ``Library/bin`` 时,
``os.add_dll_directory`` 才能找到它们。当从快捷方式 / IDE / 子进程 /
没激活的 shell 启动 ``unilab`` 时,会出现 ``DLL load failed``。
本函数会:
1) 修补 ``rclpy/impl/implementation_singleton.py`` —— rclpy 自身的 C 扩展入口;
2) 修补 ``rpyutils/add_dll_directories.py`` —— 所有 ``*_s__rosidl_typesupport_c.pyd``
``geometry_msgs`` / ``std_msgs`` / ``sensor_msgs`` 等)的统一加载入口。
打完补丁后**必须重启进程**才能生效(当前进程的 rclpy 已经发生过
``ImportError``,子模块仍处于损坏状态)。因此函数会主动退出,并在
stdout/stderr 同时打印明显的重启提示,避免用户被后续报错淹没。
"""
if sys.platform != "win32" or not os.environ.get("CONDA_PREFIX"):
return
try:
import rclpy
import rclpy # noqa: F401
return
except ImportError as e:
if not str(e).startswith("DLL load failed"):
return
cp = os.environ["CONDA_PREFIX"]
impl = os.path.join(cp, "Lib", "site-packages", "rclpy", "impl", "implementation_singleton.py")
pyd = glob.glob(os.path.join(cp, "Lib", "site-packages", "rclpy", "_rclpy_pybind11*.pyd"))
if not os.path.exists(impl) or not pyd:
lib_bin = os.path.join(cp, "Library", "bin")
site_packages = os.path.join(cp, "Lib", "site-packages")
if not os.path.isdir(lib_bin):
return
with open(impl, "r", encoding="utf-8") as f:
content = f.read()
lib_bin = os.path.join(cp, "Library", "bin").replace("\\", "/")
patch = f'# UniLabOS DLL Patch\nimport os,ctypes\nos.add_dll_directory("{lib_bin}") if hasattr(os,"add_dll_directory") else None\ntry: ctypes.CDLL("{pyd[0].replace(chr(92),"/")}")\nexcept: pass\n# End Patch\n'
shutil.copy2(impl, impl + ".bak")
with open(impl, "w", encoding="utf-8") as f:
f.write(patch + content)
patched = []
# 1) rclpy 自身的入口
rclpy_impl = os.path.join(site_packages, "rclpy", "impl", "implementation_singleton.py")
rclpy_pyd_matches = glob.glob(os.path.join(site_packages, "rclpy", "_rclpy_pybind11*.pyd"))
rclpy_pyd = rclpy_pyd_matches[0] if rclpy_pyd_matches else ""
if rclpy_pyd and _apply_dll_patch(rclpy_impl, lib_bin, preload_pyd=rclpy_pyd):
patched.append(rclpy_impl)
# 2) rpyutils —— 所有 rosidl typesupport pyd 的加载点;放在 rclpy 之后
# 例geometry_msgs/geometry_msgs_s__rosidl_typesupport_c.pyd
rpyutils_dll = os.path.join(site_packages, "rpyutils", "add_dll_directories.py")
if _apply_dll_patch(rpyutils_dll, lib_bin):
patched.append(rpyutils_dll)
if not patched:
# 已经打过补丁但 rclpy 仍然加载失败:原因不是缺 DLL 搜索路径,
# 不要再次打补丁污染文件,让上层看到真实的 ImportError。
return
_print_restart_banner(patched)
patch_rclpy_dll_windows()

View File

@@ -320,6 +320,7 @@ def job_add(req: JobAddReq) -> JobData:
action_name=action_name,
task_id=task_id,
job_id=job_id,
notebook_id=req.notebook_id,
device_action_key=device_action_key,
)

View File

@@ -59,6 +59,7 @@ class QueueItem:
action_name: str
task_id: str
job_id: str
notebook_id: str
device_action_key: str
next_run_time: float = 0 # 下次执行时间戳
retry_count: int = 0 # 重试次数
@@ -71,6 +72,7 @@ class JobInfo:
job_id: str
task_id: str
device_id: str
notebook_id: str
action_name: str
device_action_key: str
status: JobStatus
@@ -539,7 +541,10 @@ class MessageProcessor:
self.reconnect_count += 1
backoff = WSConfig.reconnect_interval
logger.info(
f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
"[MessageProcessor] 即将在 %s 秒后重连 (已尝试 %s/%s)",
backoff,
self.reconnect_count,
WSConfig.max_reconnect_attempts,
)
await asyncio.sleep(backoff)
else:
@@ -703,6 +708,7 @@ class MessageProcessor:
action_name = data.get("action_name", "")
task_id = data.get("task_id", "")
job_id = data.get("job_id", "")
notebook_id = data.get("notebook_id", "")
if not all([device_id, action_name, task_id, job_id]):
logger.error("[MessageProcessor] Missing required fields in query_action_state")
@@ -718,6 +724,7 @@ class MessageProcessor:
job_id=job_id,
task_id=task_id,
device_id=device_id,
notebook_id=notebook_id,
action_name=action_name,
device_action_key=device_action_key,
status=JobStatus.QUEUE,
@@ -732,13 +739,27 @@ class MessageProcessor:
if can_start_immediately:
# 可以立即开始
await self._send_action_state_response(
device_id, action_name, task_id, job_id, "query_action_status", True, 0
device_id,
action_name,
task_id,
job_id,
"query_action_status",
True,
0,
notebook_id=notebook_id,
)
logger.trace(f"[MessageProcessor] Job {job_log} can start immediately")
else:
# 需要排队
await self._send_action_state_response(
device_id, action_name, task_id, job_id, "query_action_status", False, 10
device_id,
action_name,
task_id,
job_id,
"query_action_status",
False,
10,
notebook_id=notebook_id,
)
logger.trace(f"[MessageProcessor] Job {job_log} queued")
@@ -768,6 +789,7 @@ class MessageProcessor:
job_id=req.job_id,
task_id=req.task_id,
device_id=req.device_id,
notebook_id=req.notebook_id,
action_name=action_name,
device_action_key=device_action_key,
status=JobStatus.QUEUE,
@@ -775,11 +797,16 @@ class MessageProcessor:
always_free=True,
)
self.device_manager.add_queue_request(job_info)
existing_job = job_info
logger.info(f"[MessageProcessor] Job {job_log} always_free, auto-registered from direct job_start")
else:
logger.error(f"[MessageProcessor] Job {job_log} not registered (missing query_action_state)")
return
if existing_job and req.notebook_id and not existing_job.notebook_id:
existing_job.notebook_id = req.notebook_id
notebook_id = req.notebook_id or (existing_job.notebook_id if existing_job else "")
success = self.device_manager.start_job(req.job_id)
if not success:
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
@@ -795,6 +822,7 @@ class MessageProcessor:
action_name=req.action,
task_id=req.task_id,
job_id=req.job_id,
notebook_id=notebook_id,
device_action_key=device_action_key,
)
@@ -834,6 +862,7 @@ class MessageProcessor:
"job_id": req.job_id,
"task_id": req.task_id,
"device_id": req.device_id,
"notebook_id": queue_item.notebook_id,
"action_name": req.action,
"status": "failed",
"feedback_data": {},
@@ -855,6 +884,7 @@ class MessageProcessor:
"query_action_status",
True,
0,
notebook_id=next_job.notebook_id,
)
next_job_log = format_job_log(
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
@@ -1004,11 +1034,16 @@ class MessageProcessor:
success = host_node.notify_resource_tree_update(dev_id, act, item_list)
if success:
if success is True:
logger.info(
f"[MessageProcessor] Resource tree {act} completed for device {dev_id}, "
f"items: {len(item_list)}"
)
elif success is None:
logger.info(
f"[MessageProcessor] Resource tree {act} skipped for device {dev_id}: "
"在线增加设备暂不支持"
)
else:
logger.warning(f"[MessageProcessor] Resource tree {act} failed for device {dev_id}")
@@ -1032,6 +1067,11 @@ class MessageProcessor:
for item in device_list:
target_node_id = item.get("target_node_id", "host_node")
if action == "add":
logger.info(
f"[DeviceManage] 在线增加设备暂不支持,跳过 add_device: {item.get('id', '')}"
)
continue
def _notify(target_id: str, act: str, cfg: ResourceDictType):
try:
@@ -1101,7 +1141,15 @@ class MessageProcessor:
logger.info(f"[MessageProcessor] Restart cleanup scheduled")
async def _send_action_state_response(
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
self,
device_id: str,
action_name: str,
task_id: str,
job_id: str,
typ: str,
free: bool,
need_more: int,
notebook_id: str = "",
):
"""发送动作状态响应"""
message = {
@@ -1112,6 +1160,7 @@ class MessageProcessor:
"action_name": action_name,
"task_id": task_id,
"job_id": job_id,
"notebook_id": notebook_id,
"free": free,
"need_more": need_more + 1,
},
@@ -1194,6 +1243,7 @@ class QueueProcessor:
action_name=timeout_job.action_name,
task_id=timeout_job.task_id,
job_id=timeout_job.job_id,
notebook_id=timeout_job.notebook_id,
device_action_key=timeout_job.device_action_key,
)
# 发布超时失败状态这会触发正常的job完成流程
@@ -1252,6 +1302,7 @@ class QueueProcessor:
"action_name": job_info.action_name,
"task_id": job_info.task_id,
"job_id": job_info.job_id,
"notebook_id": job_info.notebook_id,
"free": False,
"need_more": 10 + 1,
},
@@ -1291,6 +1342,7 @@ class QueueProcessor:
"action_name": job_info.action_name,
"task_id": job_info.task_id,
"job_id": job_info.job_id,
"notebook_id": job_info.notebook_id,
"free": False,
"need_more": 10 + 1,
},
@@ -1336,12 +1388,15 @@ class QueueProcessor:
"action_name": next_job.action_name,
"task_id": next_job.task_id,
"job_id": next_job.job_id,
"notebook_id": next_job.notebook_id,
"free": True,
"need_more": 0,
},
}
self.message_processor.send_message(message)
# next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
# next_job_log = format_job_log(
# next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
# )
# logger.debug(f"[QueueProcessor] Notified next job {next_job_log} can start")
# 立即触发下一轮状态检查
@@ -1510,6 +1565,7 @@ class WebSocketClient(BaseCommunicationClient):
"job_id": item.job_id,
"task_id": item.task_id,
"device_id": item.device_id,
"notebook_id": item.notebook_id,
"action_name": item.action_name,
"status": status,
"feedback_data": feedback_data,

View File

@@ -219,10 +219,10 @@ device = NewareBatteryTestSystem(
#### 步骤 2提交测试任务
使用 `submit_from_csv_export_ndax` 提交测试任务:
使用 `submit_from_csv` 提交测试任务:
```python
result = device.submit_from_csv_export_ndax(
result = device.submit_from_csv(
csv_path="test_data.csv",
output_dir="D:/neware_output"
)
@@ -489,7 +489,7 @@ A: 重新获取新的 Token 并更新环境变量 `UNI_LAB_AUTH_TOKEN`。
**Q: 可以自定义上传路径吗?**
A: 当前版本路径由统一 API 自动分配,`oss_prefix` 参数暂不使用(保留接口兼容性)。
**Q: 为什么不在 `submit_from_csv_export_ndax` 中自动上传?**
**Q: 为什么不在 `submit_from_csv` 中自动上传?**
A: 因为备份文件在测试进行中逐步生成,方法返回时可能文件尚未完全生成,因此提供独立的上传方法更灵活。
**Q: 上传后如何访问文件?**

View File

@@ -230,10 +230,10 @@ device = NewareBatteryTestSystem(
#### Step 2: Submit Test Tasks
Use `submit_from_csv_export_ndax` to submit test tasks:
Use `submit_from_csv` to submit test tasks:
```python
result = device.submit_from_csv_export_ndax(
result = device.submit_from_csv(
csv_path="test_data.csv",
output_dir="D:/neware_output"
)
@@ -500,7 +500,7 @@ A: Obtain a new API Key and update the `UNI_LAB_AUTH_TOKEN` environment variable
**Q: Can I customize upload paths?**
A: Current version has paths automatically assigned by unified API. `oss_prefix` parameter is currently unused (retained for interface compatibility).
**Q: Why not auto-upload in `submit_from_csv_export_ndax`?**
**Q: Why not auto-upload in `submit_from_csv`?**
A: Because backup files are generated progressively during testing, they may not be fully generated when the method returns. A separate upload method provides more flexibility.
**Q: How to access files after upload?**

View File

@@ -14,7 +14,7 @@
"config": {
"ip": "127.0.0.1",
"port": 502,
"machine_ids": [1, 2, 3, 4, 5, 6, 86],
"machine_id": 1,
"devtype": "27",
"timeout": 20,
"size_x": 500.0,
@@ -26,10 +26,10 @@
"data": {
"功能说明": "新威电池测试系统提供720通道监控和CSV批量提交功能",
"监控功能": "支持720个通道的实时状态监控、2盘电池物料管理、状态导出等",
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务NDA备份或通过submit_from_csv_export_excel action提交并备份为Excel格式。CSV必须包含: Battery_Code, Pole_Weight, 集流体质量, 活性物质含量, 克容量mah/g, 电池体系, 设备号, 排号, 通道号"
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务。CSV必须包含: Battery_Code, Pole_Weight, 集流体质量, 活性物质含量, 克容量mah/g, 电池体系, 设备号, 排号, 通道号"
},
"children": []
}
],
"links": []
}
}

View File

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

View File

@@ -2,6 +2,8 @@ import time
import logging
from typing import Union, Dict, Optional
from unilabos.registry.decorators import topic_config
class VirtualMultiwayValve:
"""
@@ -41,13 +43,11 @@ class VirtualMultiwayValve:
def target_position(self) -> int:
return self._target_position
def get_current_position(self) -> int:
"""获取当前阀门位置 📍"""
return self._current_position
def get_current_port(self) -> str:
"""获取当前连接的端口名称 🔌"""
return self._current_position
@property
@topic_config()
def current_port(self) -> str:
"""当前连接的端口名称 🔌"""
return self.port
def set_position(self, command: Union[int, str]):
"""
@@ -169,12 +169,14 @@ class VirtualMultiwayValve:
self._status = "Idle"
self._valve_state = "Closed"
close_msg = f"🔒 阀门已关闭,保持在位置 {self._current_position} ({self.get_current_port()})"
close_msg = f"🔒 阀门已关闭,保持在位置 {self._current_position} ({self.port})"
self.logger.info(close_msg)
return close_msg
def get_valve_position(self) -> int:
"""获取阀门位置 - 兼容性方法 📍"""
@property
@topic_config()
def valve_position(self) -> int:
"""阀门位置 📍"""
return self._current_position
def set_valve_position(self, command: Union[int, str]):
@@ -229,19 +231,16 @@ class VirtualMultiwayValve:
self.logger.info(f"🔄 从端口 {self._current_position} 切换到泵位置...")
return self.set_to_pump_position()
def get_flow_path(self) -> str:
"""获取当前流路路径描述 🌊"""
current_port = self.get_current_port()
@property
@topic_config()
def flow_path(self) -> str:
"""当前流路路径描述 🌊"""
if self._current_position == 0:
flow_path = f"🚰 转移泵已连接 (位置 {self._current_position})"
else:
flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})"
# 删除debug日志self.logger.debug(f"🌊 当前流路: {flow_path}")
return flow_path
return f"🚰 转移泵已连接 (位置 {self._current_position})"
return f"🔌 端口 {self._current_position} 已连接 ({self.current_port})"
def __str__(self):
current_port = self.get_current_port()
current_port = self.current_port
status_emoji = "" if self._status == "Idle" else "🔄" if self._status == "Busy" else ""
return f"🔄 VirtualMultiwayValve({status_emoji} 位置: {self._current_position}/{self.max_positions}, 端口: {current_port}, 状态: {self._status})"
@@ -253,7 +252,7 @@ if __name__ == "__main__":
print("🔄 === 虚拟九通阀门测试 === ✨")
print(f"🏠 初始状态: {valve}")
print(f"🌊 当前流路: {valve.get_flow_path()}")
print(f"🌊 当前流路: {valve.flow_path}")
# 切换到试剂瓶11号位
print(f"\n🔌 切换到1号位: {valve.set_position(1)}")

View File

@@ -3,6 +3,7 @@ import logging
import time as time_module
from typing import Dict, Any
from unilabos.registry.decorators import topic_config
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualStirrer:
@@ -314,9 +315,11 @@ class VirtualStirrer:
def min_speed(self) -> float:
return self._min_speed
def get_device_info(self) -> Dict[str, Any]:
"""获取设备状态信息 📊"""
info = {
@property
@topic_config()
def device_info(self) -> Dict[str, Any]:
"""设备状态快照信息 📊"""
return {
"device_id": self.device_id,
"status": self.status,
"operation_mode": self.operation_mode,
@@ -325,12 +328,9 @@ class VirtualStirrer:
"is_stirring": self.is_stirring,
"remaining_time": self.remaining_time,
"max_speed": self._max_speed,
"min_speed": self._min_speed
"min_speed": self._min_speed,
}
# self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}")
return info
def __str__(self):
status_emoji = "" if self.operation_mode == "Idle" else "🌪️" if self.operation_mode == "Stirring" else "🛑" if self.operation_mode == "Settling" else ""
return f"🌪️ VirtualStirrer({status_emoji} {self.device_id}: {self.operation_mode}, {self.current_speed} RPM)"

View File

@@ -4,6 +4,7 @@ from enum import Enum
from typing import Union, Optional
import logging
from unilabos.registry.decorators import topic_config
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
@@ -385,8 +386,10 @@ class VirtualTransferPump:
"""获取当前体积"""
return self._current_volume
def get_remaining_capacity(self) -> float:
"""获取剩余容量"""
@property
@topic_config()
def remaining_capacity(self) -> float:
"""剩余容量 (ml)"""
return self.max_volume - self._current_volume
def is_empty(self) -> bool:

View File

@@ -14,20 +14,30 @@ Virtual Workbench Device - 模拟工作台设备
import logging
import time
from typing import Dict, Any, Optional, List
from dataclasses import dataclass
from enum import Enum
from threading import Lock, RLock
from typing import Any, Dict, List, Optional, cast
from typing_extensions import TypedDict
from unilabos.registry.decorators import (
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action, NodeType
ActionInputHandle,
ActionOutputHandle,
DataSource,
NodeType,
action,
device,
not_action,
topic_config,
)
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, ResourceTreeSet
from unilabos.resources.resource_tracker import (
SampleUUIDsType,
LabSample,
ResourceTreeSet,
)
# ============ TypedDict 返回类型定义 ============
@@ -112,6 +122,7 @@ class HeatingStation:
@device(
id="virtual_workbench",
display_name="虚拟工作台",
category=["virtual_device"],
description="Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent material processing",
)
@@ -137,7 +148,19 @@ class VirtualWorkbench:
HEATING_TIME: float = 60.0 # 加热时间(秒)
NUM_HEATING_STATIONS: int = 3 # 加热台数量
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
def __init__(
self,
device_id: Optional[str] = None,
config: Optional[Dict[str, Any]] = None,
**kwargs,
):
"""
初始化虚拟工作台。
Args:
device_id[设备ID]: 工作台设备实例 ID默认使用 virtual_workbench。
config[设备配置]: 可包含 arm_operation_time、heating_time、num_heating_stations。
"""
# 处理可能的不同调用方式
if device_id is None and "id" in kwargs:
device_id = kwargs.pop("id")
@@ -151,9 +174,13 @@ class VirtualWorkbench:
self.data: Dict[str, Any] = {}
# 从config中获取可配置参数
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", self.ARM_OPERATION_TIME))
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.NUM_HEATING_STATIONS = int(
self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS)
)
# 机械臂状态和锁
self._arm_lock = Lock()
@@ -162,7 +189,8 @@ class VirtualWorkbench:
# 加热台状态
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()
@@ -292,45 +320,113 @@ class VirtualWorkbench:
self.logger.info(f"机械臂已释放 (完成: {task})")
@action(
always_free=True, node_type=NodeType.MANUAL_CONFIRM, placeholder_keys={
"assignee_user_ids": "unilabos_manual_confirm"
}, goal_default={
"timeout_seconds": 3600,
"assignee_user_ids": []
}, feedback_interval=300,
always_free=True,
node_type=NodeType.MANUAL_CONFIRM,
placeholder_keys={"assignee_user_ids": "unilabos_manual_confirm"},
goal_default={"timeout_seconds": 3600, "assignee_user_ids": []},
feedback_interval=300,
handles=[
ActionInputHandle(key="target_device", data_type="device_id",
label="目标设备", data_key="target_device", data_source=DataSource.HANDLE),
ActionInputHandle(key="resource", data_type="resource",
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="mount_resource", data_type="resource",
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="collector_mass", data_type="collector_mass",
label="极流体质量", data_key="collector_mass", data_source=DataSource.HANDLE),
ActionInputHandle(key="active_material", data_type="active_material",
label="活性物质含量", data_key="active_material", data_source=DataSource.HANDLE),
ActionInputHandle(key="capacity", data_type="capacity",
label="克容量", data_key="capacity", data_source=DataSource.HANDLE),
ActionInputHandle(key="battery_system", data_type="battery_system",
label="电池体系", data_key="battery_system", data_source=DataSource.HANDLE),
ActionInputHandle(
key="target_device",
data_type="device_id",
label="目标设备",
data_key="target_device",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="resource",
data_type="resource",
label="待转移资源",
data_key="resource",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="mount_resource",
data_type="resource",
label="目标孔位",
data_key="mount_resource",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="collector_mass",
data_type="collector_mass",
label="极流体质量",
data_key="collector_mass",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="active_material",
data_type="active_material",
label="活性物质含量",
data_key="active_material",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="capacity",
data_type="capacity",
label="克容量",
data_key="capacity",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="battery_system",
data_type="battery_system",
label="电池体系",
data_key="battery_system",
data_source=DataSource.HANDLE,
),
# transfer使用
ActionOutputHandle(key="target_device", data_type="device_id",
label="目标设备", data_key="target_device", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="resource", data_type="resource",
label="待转移资源", data_key="resource.@flatten", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="mount_resource", data_type="resource",
label="目标孔位", data_key="mount_resource.@flatten", data_source=DataSource.EXECUTOR),
ActionOutputHandle(
key="target_device",
data_type="device_id",
label="目标设备",
data_key="target_device",
data_source=DataSource.EXECUTOR,
),
ActionOutputHandle(
key="resource",
data_type="resource",
label="待转移资源",
data_key="resource.@flatten",
data_source=DataSource.EXECUTOR,
),
ActionOutputHandle(
key="mount_resource",
data_type="resource",
label="目标孔位",
data_key="mount_resource.@flatten",
data_source=DataSource.EXECUTOR,
),
# test使用
ActionOutputHandle(key="collector_mass", data_type="collector_mass",
label="极流体质量", data_key="collector_mass", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="active_material", data_type="active_material",
label="活性物质含量", data_key="active_material", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="capacity", data_type="capacity",
label="克容量", data_key="capacity", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="battery_system", data_type="battery_system",
label="电池体系", data_key="battery_system", data_source=DataSource.EXECUTOR),
]
ActionOutputHandle(
key="collector_mass",
data_type="collector_mass",
label="极流体质量",
data_key="collector_mass",
data_source=DataSource.EXECUTOR,
),
ActionOutputHandle(
key="active_material",
data_type="active_material",
label="活性物质含量",
data_key="active_material",
data_source=DataSource.EXECUTOR,
),
ActionOutputHandle(
key="capacity",
data_type="capacity",
label="克容量",
data_key="capacity",
data_source=DataSource.EXECUTOR,
),
ActionOutputHandle(
key="battery_system",
data_type="battery_system",
label="电池体系",
data_key="battery_system",
data_source=DataSource.EXECUTOR,
),
],
)
def manual_confirm(
self,
@@ -343,67 +439,156 @@ class VirtualWorkbench:
battery_system: List[str],
timeout_seconds: int,
assignee_user_ids: list[str],
**kwargs
**kwargs,
) -> dict:
"""
timeout_seconds: 超时时间默认3600秒
collector_mass: 极流体质量
active_material: 活性物质含量
capacity: 克容量mAh/g
battery_system: 电池体系
修改的结果无效,是只读的
人工确认资源转移和扣电测试参数。
Args:
resource[待转移资源]: 需要人工确认的资源列表。
target_device[目标设备]: 资源要转移到的目标设备 ID。
mount_resource[目标孔位]: 资源要挂载到的目标孔位列表。
collector_mass[极流体质量]: 每个样品对应的极流体质量。
active_material[活性物质含量]: 每个样品对应的活性物质含量。
capacity[克容量]: 每个样品对应的克容量,单位 mAh/g。
battery_system[电池体系]: 每个样品对应的电池体系名称。
timeout_seconds[超时时间]: 人工确认超时时间,单位秒。
assignee_user_ids[确认人]: 指定处理人工确认任务的用户 ID 列表。
Note:
修改的结果无效,是只读的。
"""
resource = ResourceTreeSet.from_plr_resources(resource).dump()
mount_resource = ResourceTreeSet.from_plr_resources(mount_resource).dump()
resource_tree = ResourceTreeSet.from_plr_resources(cast(Any, resource)).dump()
mount_resource_tree = ResourceTreeSet.from_plr_resources(cast(Any, mount_resource)).dump()
kwargs.update(locals())
kwargs.pop("kwargs")
kwargs.pop("self")
kwargs["resource"] = resource_tree
kwargs["mount_resource"] = mount_resource_tree
kwargs.pop("resource_tree")
kwargs.pop("mount_resource_tree")
return kwargs
@action(
description="转移物料",
handles=[
ActionInputHandle(key="target_device", data_type="device_id",
label="目标设备", data_key="target_device", data_source=DataSource.HANDLE),
ActionInputHandle(key="resource", data_type="resource",
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="mount_resource", data_type="resource",
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
]
ActionInputHandle(
key="target_device",
data_type="device_id",
label="目标设备",
data_key="target_device",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="resource",
data_type="resource",
label="待转移资源",
data_key="resource",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="mount_resource",
data_type="resource",
label="目标孔位",
data_key="mount_resource",
data_source=DataSource.HANDLE,
),
],
)
async def transfer(self, resource: List[ResourceSlot], target_device: DeviceSlot, mount_resource: List[ResourceSlot]):
future = ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True,
async def transfer(
self,
resource: List[ResourceSlot],
target_device: DeviceSlot,
mount_resource: List[ResourceSlot],
):
"""
转移资源到目标设备。
Args:
resource[待转移资源]: 待转移的资源列表。
target_device[目标设备]: 接收资源的目标设备 ID。
mount_resource[目标孔位]: 目标设备上的挂载孔位列表。
"""
future = ROS2DeviceNode.run_async_func(
self._ros_node.transfer_resource_to_another,
True,
**{
"plr_resources": resource,
"target_device_id": target_device,
"target_resources": mount_resource,
"sites": [None] * len(mount_resource),
})
},
)
result = await future
return result
@action(
description="扣电测试启动",
handles=[
ActionInputHandle(key="resource", data_type="resource",
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="mount_resource", data_type="resource",
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
ActionInputHandle(key="collector_mass", data_type="collector_mass",
label="极流体质量", data_key="collector_mass", data_source=DataSource.HANDLE),
ActionInputHandle(key="active_material", data_type="active_material",
label="活性物质含量", data_key="active_material", data_source=DataSource.HANDLE),
ActionInputHandle(key="capacity", data_type="capacity",
label="克容量", data_key="capacity", data_source=DataSource.HANDLE),
ActionInputHandle(key="battery_system", data_type="battery_system",
label="电池体系", data_key="battery_system", data_source=DataSource.HANDLE),
]
ActionInputHandle(
key="resource",
data_type="resource",
label="待转移资源",
data_key="resource",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="mount_resource",
data_type="resource",
label="目标孔位",
data_key="mount_resource",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="collector_mass",
data_type="collector_mass",
label="极流体质量",
data_key="collector_mass",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="active_material",
data_type="active_material",
label="活性物质含量",
data_key="active_material",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="capacity",
data_type="capacity",
label="克容量",
data_key="capacity",
data_source=DataSource.HANDLE,
),
ActionInputHandle(
key="battery_system",
data_type="battery_system",
label="电池体系",
data_key="battery_system",
data_source=DataSource.HANDLE,
),
],
)
async def test(
self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], collector_mass: List[float], active_material: List[float], capacity: List[float], battery_system: list[str]
self,
resource: List[ResourceSlot],
mount_resource: List[ResourceSlot],
collector_mass: List[float],
active_material: List[float],
capacity: List[float],
battery_system: list[str],
):
"""
启动扣电测试。
Args:
resource[待测试资源]: 需要进行扣电测试的资源列表。
mount_resource[测试孔位]: 扣电测试使用的目标孔位列表。
collector_mass[极流体质量]: 每个样品对应的极流体质量。
active_material[活性物质含量]: 每个样品对应的活性物质含量。
capacity[克容量]: 每个样品对应的克容量,单位 mAh/g。
battery_system[电池体系]: 每个样品对应的电池体系名称。
"""
print(resource)
print(mount_resource)
print(collector_mass)
@@ -415,16 +600,11 @@ class VirtualWorkbench:
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),
ActionOutputHandle(key="channel_1", data_type="workbench_material", label="实验1", data_key="material_1", data_source=DataSource.EXECUTOR), # noqa: E501
ActionOutputHandle(key="channel_2", data_type="workbench_material", label="实验2", data_key="material_2", data_source=DataSource.EXECUTOR), # noqa: E501
ActionOutputHandle(key="channel_3", data_type="workbench_material", label="实验3", data_key="material_3", data_source=DataSource.EXECUTOR), # noqa: E501
ActionOutputHandle(key="channel_4", data_type="workbench_material", label="实验4", data_key="material_4", data_source=DataSource.EXECUTOR), # noqa: E501
ActionOutputHandle(key="channel_5", data_type="workbench_material", label="实验5", data_key="material_5", data_source=DataSource.EXECUTOR), # noqa: E501
],
)
def prepare_materials(
@@ -437,6 +617,9 @@ class VirtualWorkbench:
作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。
输出5个handle (material_1 ~ material_5), 分别对应实验1~5。
Args:
count[物料数量]: 要生成的物料数量,默认生成 5 个。
"""
materials = [i for i in range(1, count + 1)]
@@ -457,7 +640,11 @@ class VirtualWorkbench:
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra={"material_uuid": content} if isinstance(content, str) else (content.serialize() if content else {}),
extra=(
{"material_uuid": content}
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
@@ -467,12 +654,27 @@ class VirtualWorkbench:
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),
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(
@@ -484,6 +686,9 @@ class VirtualWorkbench:
将物料从An位置移动到加热台
多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台
Args:
material_number[物料编号]: 要移动的物料编号,对应 A1、A2 等起始位置。
"""
material_id = f"A{material_number}"
task_desc = f"移动{material_id}到加热台"
@@ -546,7 +751,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -569,7 +775,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -581,14 +788,34 @@ class VirtualWorkbench:
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),
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(
@@ -599,6 +826,10 @@ class VirtualWorkbench:
) -> StartHeatingResult:
"""
启动指定加热台的加热程序
Args:
station_id[加热台ID]: 要启动加热的加热台编号。
material_number[物料编号]: 当前加热台上的物料编号。
"""
self.logger.info(f"[加热台{station_id}] 开始加热")
@@ -615,7 +846,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -638,7 +870,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -658,7 +891,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -698,7 +932,9 @@ 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")
self.logger.info(
f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s"
)
last_countdown_log = time.time()
if elapsed >= self.HEATING_TIME:
@@ -715,7 +951,9 @@ class VirtualWorkbench:
self._active_tasks[material_id]["status"] = "heating_completed"
self._update_data_status(f"加热台{station_id}加热完成")
self.logger.info(f"[加热台{station_id}] {material_id}加热完成 (用时{self.HEATING_TIME}s)")
self.logger.info(
f"[加热台{station_id}] {material_id}加热完成 (用时{self.HEATING_TIME}s)"
)
return {
"success": True,
@@ -729,7 +967,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -740,10 +979,20 @@ class VirtualWorkbench:
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),
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(
@@ -754,6 +1003,10 @@ class VirtualWorkbench:
) -> MoveToOutputResult:
"""
将物料从加热台移动到输出位置Cn
Args:
station_id[加热台ID]: 已完成加热的加热台编号。
material_number[物料编号]: 要移动到输出位置的物料编号,对应 Cn。
"""
output_number = material_number
@@ -770,7 +1023,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -794,7 +1048,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -814,7 +1069,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
@@ -896,7 +1152,8 @@ class VirtualWorkbench:
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
if isinstance(content, str)
else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()

View File

@@ -258,7 +258,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
logger.info(f"[同步→Bioyond] 物料不存在于 Bioyond将创建新物料并入库")
# 第1步从配置中获取仓库配置
warehouse_mapping = self.workstation.bioyond_config.get("warehouse_mapping", {})
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {})
# 确定目标仓库名称
parent_name = None
@@ -760,9 +760,10 @@ class BioyondWorkstation(WorkstationBase):
except:
pass
# 创建通信模块;同步器将在 post_init 中初始化并执行首次同步
# 创建通信模块
self._create_communication_module(bioyond_config)
self.resource_synchronizer = None
self.resource_synchronizer = BioyondResourceSynchronizer(self)
self.resource_synchronizer.sync_from_external()
# TODO: self._ros_node里面拿属性
@@ -801,15 +802,6 @@ class BioyondWorkstation(WorkstationBase):
def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node
# Deck 为空时(反序列化未恢复子节点),主动调用 setup() 初始化仓库
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
logger.info("Deck 无仓库子节点,调用 setup() 初始化仓库")
self.deck.setup()
# 初始化同步器并执行首次同步(需在仓库初始化之后)
self.resource_synchronizer = BioyondResourceSynchronizer(self)
self.resource_synchronizer.sync_from_external()
# 启动连接监控
try:
self.connection_monitor = ConnectionMonitor(self)

View File

@@ -1,219 +0,0 @@
# 代码变更说明 — 2026-03-12
> 本次变更基于 `implementation_plan_v2.md` 执行,目标:**物理几何结构初始化与物料内容物填充彻底解耦**,消除 PLR 反序列化时的 `Resource already assigned to deck` 错误,并修复若干运行时新增问题。
---
## 一、物料系统标准化重构(主线任务)
### 1. `unilabos/resources/battery/magazine.py`
**改动**`MagazineHolder_6_Cathode``MagazineHolder_6_Anode``MagazineHolder_4_Cathode` 三个工厂函数的 `klasses` 参数改为 `None`
**原因**:原来三个工厂函数在初始化时就向洞位填满极片对象(`ElectrodeSheet`),导致 PLR 反序列化时"几何结构已创建子节点 + DB 再次 assign"双重冲突。
**原则**:物料余量改由寄存器直读(阶段 F资源树不再追踪每个极片实体。`MagazineHolder_6_Battery` 原本就是 `klasses=None`,三者现在保持一致。
---
### 2. `unilabos/resources/battery/magazine.py`(追加,响应重复 UUID 问题)
**改动**:为 `Magazine`(洞位类)新增 `serialize``deserialize` 重写:
- `serialize`:序列化时强制将 `children` 置空,不再把极片写回数据库。
- `deserialize`:反序列化时强制忽略 `children` 字段,阻止数据库中旧极片记录被恢复。
**原因**:数据库中遗留有旧的 `ElectrodeSheet` 记录(`A1_sheet100` 等),启动时被 PLR 反序列化进来,导致同一 UUID 出现在多个 Magazine 洞位中,触发 `发现重复的uuid` 错误。此修复从源头截断旧数据,经过一次完整的"启动 → 资源树写回"后,数据库旧极片记录也会被干净覆盖。
---
### 3. `unilabos/resources/battery/bottle_carriers.py`
**改动**:删除 `YIHUA_Electrolyte_12VialCarrier` 末尾的 12 瓶填充循环及对应 `import`
**原因**`bottle_rack_6x2``bottle_rack_6x2_2` 应初始化为空载架,瓶子由 Bioyond 侧实际转运后再填入。原来初始化时直接塞满 `YB_pei_ye_xiao_Bottle`,反序列化时产生重复 assign。
---
### 4. `unilabos/resources/bioyond/decks.py`
**改动**
-`BIOYOND_YB_Deck` 重命名为 `BioyondElectrolyteDeck`,保留 `BIOYOND_YB_Deck` 作为向后兼容别名。
- 工厂函数 `YB_Deck()` 重命名为 `bioyond_electrolyte_deck()`,保留 `YB_Deck` 作为别名。
- `BIOYOND_PolymerReactionStation_Deck``BIOYOND_PolymerPreparationStation_Deck``BioyondElectrolyteDeck` 三个 Deck 类:
- 移除 `__init__` 中的 `setup: bool = False` 参数及 `if setup: self.setup()` 调用。
- 删除临时 `deserialize` 补丁(该补丁是为了强制 `setup=False`,根本原因消除后不再需要)。
**原因**`setup` 参数导致 PLR 反序列化时先通过 `__init__` 创建所有子资源,再从 JSON `children` 字段再次 assign产生 `already assigned to deck` 错误。正确模式:`__init__` 只初始化自身几何,`setup()` 由工厂函数调用,反序列化由 PLR 从 DB 数据重建子资源。
---
### 5. `unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`
**改动**
- `CoincellDeck` 重命名为 `YihuaCoinCellDeck`,保留 `CoincellDeck` 作为向后兼容别名。
- 工厂函数 `YH_Deck()` 重命名为 `yihua_coin_cell_deck()`,保留 `YH_Deck` 作为别名。
- 移除 `YihuaCoinCellDeck.__init__` 中的 `setup: bool = False` 参数及调用,删除 `deserialize` 补丁(原因同 decks.py
- `MaterialPlate.__init__` 移除 `fill` 参数和 `fill=True` 分支,新增类方法 `MaterialPlate.create_with_holes()` 作为"带洞位"的工厂方法,`setup()` 改为调用该工厂方法。
- `YihuaCoinCellDeck.setup()` 末尾新增 `electrolyte_buffer``ResourceStack`)接驳槽,用于接收来自 Bioyond 侧的分液瓶板,命名与 `bioyond_cell_workstation.py``sites=["electrolyte_buffer"]` 一致。
---
### 6. `unilabos/resources/resource_tracker.py`
**改动 1**`to_plr_resources` 中,`load_all_state` 调用前预填 `Container` 类资源缺失的键:
```python
state.setdefault("liquid_history", [])
state.setdefault("pending_liquids", {})
```
**原因**:新版 PLR 要求 `Container` 状态中必须包含这两个键,旧数据库记录缺失时 `load_all_state` 会抛出 `KeyError`
**改动 2**`_validate_tree` 中,遇到重复 UUID 时改为自动重新分配新 UUID 并打 `WARNING`,不再直接抛异常崩溃。
**原因**:旧数据库中存在多个同名同 UUID 的极片对象(历史脏数据),严格校验会导致节点无法启动。改为 WARNING + 自动修复,确保启动成功,下次资源树写回后脏数据自然清除。
---
### 7. `unilabos/resources/itemized_carrier.py`
**改动**:将原来的 `idx is None` 兜底补丁(静默调用 `super().assign_child_resource`,不更新槽位追踪)替换为两段式逻辑:
1. **XY 近似匹配**(容差 2mm精确三维坐标匹配失败时仅对比 XY 二维坐标,找到最近槽位后用槽位的正确坐标(含 Z完成 assign并打 `WARNING`
2. **XY 也失败才抛异常**:给出详细的槽位列表和传入坐标,便于问题排查。
**原因**:数据库中存储的资源坐标 Z=0`warehouse_factory` 定义的槽位 Z=dz如 10mm。精确匹配永远失败原补丁静默兜底掩盖了这一问题。近似匹配修复了 Z 偏移,同时保留了真正异常时的报错能力。
---
### 8. `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
**改动 1**:更新导入:`BIOYOND_YB_Deck``BioyondElectrolyteDeck, bioyond_electrolyte_deck`
**改动 2**`__main__` 入口处改为调用 `bioyond_electrolyte_deck(name="YB_Deck")`
**改动 3**:新增 `_get_resource_from_device(device_id, resource_name)` 方法,用于从目标设备的资源树中动态查找 PLR 资源对象(带降级回退逻辑)。
**改动 4**:跨站转运逻辑中,将原来"创建 `size=1,1,1` 的虚拟 `ResourcePLR` + 硬编码 UUID"的方式,改为通过 `_get_resource_from_device` 从目标设备获取真实的 `electrolyte_buffer` 资源对象。
**原因**:原代码使用硬编码 UUID 的虚拟资源作为转运目标,该对象在 YihuaCoinCellDeck 的资源树中不存在,转移后资源树状态混乱。
---
### 9. `unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`
**改动 1**:更新导入:`CoincellDeck``YihuaCoinCellDeck, yihua_coin_cell_deck``__main__` 入口改为调用 `yihua_coin_cell_deck()`
**改动 2**:新增 10 个 `@property`,实现对依华扣电工站 Modbus 寄存器的直读:
| 属性名 | 寄存器地址 | 说明 |
|---|---|---|
| `data_10mm_positive_plate_remaining` | 520 | 10mm正极片余量 |
| `data_12mm_positive_plate_remaining` | 522 | 12mm正极片余量 |
| `data_16mm_positive_plate_remaining` | 524 | 16mm正极片余量 |
| `data_aluminum_foil_remaining` | 526 | 铝箔余量 |
| `data_positive_shell_remaining` | 528 | 正极壳余量 |
| `data_flat_washer_remaining` | 530 | 平垫余量 |
| `data_negative_shell_remaining` | 532 | 负极壳余量 |
| `data_spring_washer_remaining` | 534 | 弹垫余量 |
| `data_finished_battery_remaining_capacity` | 536 | 成品电池余量 |
| `data_finished_battery_ng_remaining_capacity` | 538 | 成品电池NG槽余量 |
**原因**`coin_cell_workstation.yaml``status_types` 中定义了这 10 个属性,但代码中从未实现,导致每次前端轮询时均报 `AttributeError`
---
## 二、配置与注册表更新
### 10. `yibin_electrolyte_config.json`
- `BIOYOND_YB_Deck``BioyondElectrolyteDeck`class、type、_resource_type 三处)
- `CoincellDeck``YihuaCoinCellDeck`class、type、_resource_type 三处)
- 移除 `"setup": true` 字段
### 11. `yibin_coin_cell_only_config.json`
- `CoincellDeck``YihuaCoinCellDeck`
- 移除 `"setup": true`
### 12. `yibin_electrolyte_only_config.json`
- `BIOYOND_YB_Deck``BioyondElectrolyteDeck`
- 移除 `"setup": true`
### 13. `unilabos/registry/resources/bioyond/deck.yaml`
- `BIOYOND_YB_Deck``BioyondElectrolyteDeck`,工厂函数路径更新为 `bioyond_electrolyte_deck`
- `CoincellDeck``YihuaCoinCellDeck`,工厂函数路径更新为 `yihua_coin_cell_deck`
---
## 三、独立 Bug 修复
### 14. `unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv`
**改动**10 条余量寄存器记录的 `DataType` 列从 `REAL` 改为 `FLOAT32`
**原因**`REAL` 是 IEC 61131-3 PLC 工程师惯用名称,但 pymodbus 的 `DATATYPE` 枚举只有 `FLOAT32``DataType['REAL']` 查表时抛 `KeyError: 'REAL'`,导致 `CoinCellAssemblyWorkstation` 节点启动失败。
---
## 四、运行期新增 Bug 修复第二轮2026-03-12 18:12 日志)
### 15. `unilabos/devices/workstation/bioyond_studio/station.py`
**改动**:第 261 行 `self.bioyond_config``self.workstation.bioyond_config`
**原因**`BioyondResourceSynchronizer.sync_to_external` 内部误用了 `self.bioyond_config`,而该类从未设置此属性(应通过 `self.workstation.bioyond_config` 访问)。触发场景:用户在前端将任意物料拖入仓库时,同步到 Bioyond 必定抛出 `AttributeError: 'BioyondResourceSynchronizer' object has no attribute 'bioyond_config'`
---
### 16. `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
**改动**`_get_type_id_by_name` 方法新增"直接英文 key 命中"分支:
- **原逻辑**:仅按 `value[0]`(中文名,如 `"5ml分液瓶板"`)遍历比较。
- **新逻辑**:先以 `type_name` 直接查找 `material_type_mappings` 字典 key英文 model 名,如 `"YB_Vial_5mL_Carrier"`),命中则立即返回 UUID否则再按中文名兜底遍历。
**原因**`resource_tree_transfer``plr_resource.model`(英文 key作为 `board_type` / `bottle_type` 传给 `create_sample`,后者再调用 `_get_type_id_by_name`。旧版函数只按中文名查,导致英文 key 永远匹配不到 → `ValueError: 未找到板类型 'YB_Vial_5mL_Carrier' 的配置`。新函数兼容两种查找方式,同时保持向后兼容。
---
## 五、运行期新增 Bug 修复第三轮2026-03-12 20:30 日志)
### 17. `unilabos/resources/resource_tracker.py`(追加)
**改动**:在 `to_plr_resources` 中,`sub_cls.deserialize` 调用前新增 `_deduplicate_plr_dict(plr_dict)` 预处理函数。
**函数逻辑**:递归遍历整个 `plr_dict` 树,在**全树范围**对 `children` 列表按 `name` 去重——保留首次出现的同名节点,跳过重复项并打 `WARNING`
**根本原因**
1. 用户通过前端将 `YB_Vial_5mL_Carrier` 拖入仓库 E01carrier 及其子 vial`YB_Vial_5mL_Carrier_vial_A1` 等)被写入数据库。
2. 随后 `sync_from_external`Bioyond 定期同步)以**新 UUID** 重新创建同名 carrier 并赋给同一槽位PLR 内存树中的旧 carrier 被替换,但**数据库旧记录未被清除**。
3. 下次重启时,数据库同一 `WareHouse` 下存在两条同名 `BottleCarrier`(不同 UUID`node_to_plr_dict` 将二者都放入 `children` 列表PLR 反序列化第二个 carrier 时子 vial 命名冲突,抛出 `ValueError: Resource with name 'YB_Vial_5mL_Carrier_vial_A1' already exists in the tree.`,整个 deck 无法加载,系统启动失败。
**连锁错误(随根因修复自动消除)**
- `TypeError: Deck.__init__() got an unexpected keyword argument 'data'` — deck 加载失败后 `driver_creator.py` 触发降级路径,参数类型错误
- `AttributeError: 'ResourceDictInstance' object has no attribute 'copy'` — 另一条降级路径失败
- `ValueError: Deck 配置不能为空` — 所有 deck 创建路径失败,`deck=None` 传入工作站
---
> **验证状态**2026-03-12 20:56 日志确认系统正常运行,无新增 ERROR 级错误。
---
## 六、变更文件汇总(最终)
| 文件 | 变更类型 | 轮次 |
|---|---|---|
| `resources/battery/magazine.py` | 重构 + Bug 修复(极片子节点解耦 + 旧数据清理) | 第一轮 |
| `resources/battery/bottle_carriers.py` | 重构(移除初始化时自动填瓶) | 第一轮 |
| `resources/bioyond/decks.py` | 重构 + 重命名BioyondElectrolyteDeck | 第一轮 |
| `devices/workstation/coin_cell_assembly/YB_YH_materials.py` | 重构 + 重命名YihuaCoinCellDeck+ 新增 electrolyte_buffer 槽位 | 第一轮 |
| `resources/resource_tracker.py` | Bug 修复 × 3Container 状态键预填 + 重复 UUID 自动修复 + 树级名称去重) | 第一/三轮 |
| `resources/itemized_carrier.py` | Bug 修复XY 近似坐标匹配,修复 Z 偏移) | 第一轮 |
| `devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py` | 重构 + Bug 修复(跨站转运 + 类型映射双模式查找) | 第一/二轮 |
| `devices/workstation/bioyond_studio/station.py` | Bug 修复sync_to_external 属性访问路径) | 第二轮 |
| `devices/workstation/coin_cell_assembly/coin_cell_assembly.py` | 新增 10 个 Modbus 余量属性 + 更新导入 | 第一轮 |
| `yibin_electrolyte_config.json` | 配置更新(类名 + 移除 setup | 第一轮 |
| `yibin_coin_cell_only_config.json` | 配置更新(类名 + 移除 setup | 第一轮 |
| `yibin_electrolyte_only_config.json` | 配置更新(类名 + 移除 setup | 第一轮 |
| `registry/resources/bioyond/deck.yaml` | 注册表更新(类名 + 工厂函数路径) | 第一轮 |
| `devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv` | Bug 修复REAL → FLOAT32 | 第一轮 |

View File

@@ -130,14 +130,20 @@ class MaterialPlate(ItemizedResource[MaterialHole]):
ordering: Optional[OrderedDict[str, str]] = None,
category: str = "material_plate",
model: Optional[str] = None,
fill: bool = False
):
"""初始化料板(不主动填充洞位,由工厂方法或反序列化恢复)
"""初始化料板
Args:
name: 料板名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing_x: X方向洞位间距 (mm)
hole_spacing_y: Y方向洞位间距 (mm)
number: 编号
category: 类别
model: 型号
"""
@@ -147,50 +153,42 @@ class MaterialPlate(ItemizedResource[MaterialHole]):
hole_diameter=20.0,
info="",
)
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=ordered_items,
ordering=ordering,
category=category,
model=model,
)
@classmethod
def create_with_holes(
cls,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str = "material_plate",
model: Optional[str] = None,
) -> "MaterialPlate":
"""工厂方法:创建带 4x4 洞位的料板(仅用于初始 setup不在反序列化路径调用"""
# 默认洞位间距(与 _unilabos_state 默认值保持一致)
hole_spacing_x = 24.0
hole_spacing_y = 24.0
# 先建洞位,再作为 ordered_items 传入构造函数
# ItemizedResource.__init__ 要求 ordered_items 或 ordering 二选一必须有值)
# 创建4x4的洞位
# TODO: 这里要改,对应不同形状
holes = create_ordered_items_2d(
klass=MaterialHole,
num_items_x=4,
num_items_y=4,
dx=(size_x - 4 * hole_spacing_x) / 2,
dy=(size_y - 4 * hole_spacing_y) / 2,
dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
dz=size_z,
item_dx=hole_spacing_x,
item_dy=hole_spacing_y,
size_x=16,
size_y=16,
size_z=16,
)
return cls(
name=name, size_x=size_x, size_y=size_y, size_z=size_z,
ordered_items=holes, category=category, model=model,
item_dx=self._unilabos_state["hole_spacing_x"],
item_dy=self._unilabos_state["hole_spacing_y"],
size_x = 16,
size_y = 16,
size_z = 16,
)
if fill:
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
else:
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=ordered_items,
ordering=ordering,
category=category,
model=model,
)
def update_locations(self):
# TODO:调多次相加
@@ -536,19 +534,30 @@ class WasteTipBox(Trash):
return data
class YihuaCoinCellDeck(Deck):
"""依华纽扣电池组装工作站台面类"""
class CoincellDeck(Deck):
"""纽扣电池组装工作站台面类"""
def __init__(
self,
name: str = "coin_cell_deck",
size_x: float = 1450.0,
size_y: float = 1450.0,
size_z: float = 100.0,
size_x: float = 1450.0, # 1m
size_y: float = 1450.0, # 1m
size_z: float = 100.0, # 0.9m
origin: Coordinate = Coordinate(-2200, 0, 0),
category: str = "coin_cell_deck",
setup: bool = False,
setup: bool = False, # 是否自动执行 setup
):
"""初始化纽扣电池组装工作站台面
Args:
name: 台面名称
size_x: 长度 (mm) - 1m
size_y: 宽度 (mm) - 1m
size_z: 高度 (mm) - 0.9m
origin: 原点坐标
category: 类别
setup: 是否自动执行 setup 配置标准布局
"""
super().__init__(
name=name,
size_x=1450.0,
@@ -582,11 +591,14 @@ class YihuaCoinCellDeck(Deck):
# ====================================== 物料板 ============================================
# 创建物料板料盘carrier- 4x4布局
# 负极料盘
fujiliaopan = MaterialPlate.create_with_holes(name="负极料盘", size_x=120, size_y=100, size_z=10.0)
fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(fujiliaopan, Coordinate(x=708.0, y=794.0, z=0))
# for i in range(16):
# fujipian = ElectrodeSheet(name=f"{fujiliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
# fujiliaopan.children[i].assign_child_resource(fujipian, location=None)
# 隔膜料盘
gemoliaopan = MaterialPlate.create_with_holes(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0)
gemoliaopan = MaterialPlate(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(gemoliaopan, Coordinate(x=718.0, y=918.0, z=0))
# for i in range(16):
# gemopian = ElectrodeSheet(name=f"{gemoliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
@@ -621,27 +633,11 @@ class YihuaCoinCellDeck(Deck):
waste_tip_box = WasteTipBox(name="waste_tip_box")
self.assign_child_resource(waste_tip_box, Coordinate(x=778.0, y=622.0, z=0))
# 分液瓶板接驳区 - 接收来自 BioyondElectrolyte 侧的完整 Vial Carrier 板
# 命名 electrolyte_buffer 与 bioyond_cell_workstation.py 中 sites=["electrolyte_buffer"] 对应
electrolyte_buffer = ResourceStack(
name="electrolyte_buffer",
direction="z",
resources=[],
)
self.assign_child_resource(electrolyte_buffer, Coordinate(x=1050.0, y=700.0, z=0))
def yihua_coin_cell_deck(name: str = "coin_cell_deck") -> YihuaCoinCellDeck:
deck = YihuaCoinCellDeck(name=name)
deck.setup()
return deck
# 向后兼容别名,日后废弃
CoincellDeck = YihuaCoinCellDeck
def YH_Deck(name: str = "") -> YihuaCoinCellDeck:
return yihua_coin_cell_deck(name=name or "coin_cell_deck")
def YH_Deck(name=""):
cd = CoincellDeck(name=name)
cd.setup()
return cd
if __name__ == "__main__":

View File

@@ -6,7 +6,7 @@ import threading
import time
import types
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional
from functools import wraps
from pylabrobot.resources import Deck, Resource as PLRResource
from unilabos_msgs.msg import Resource
@@ -17,7 +17,7 @@ from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNo
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import *
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import YihuaCoinCellDeck, yihua_coin_cell_deck
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
from unilabos.resources.graphio import convert_resources_to_type
from unilabos.utils.log import logger
import struct
@@ -161,9 +161,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
logger.info("没有传入依华deck检查启动json文件")
super().__init__(deck=deck, *args, **kwargs,)
self.debug_mode = debug_mode
self._modbus_address = address
self._modbus_port = port
""" 连接初始化 """
modbus_client = TCPClient(addr=address, port=port)
logger.debug(f"创建 Modbus 客户端: {modbus_client}")
@@ -180,11 +178,9 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
raise ValueError('modbus tcp connection failed')
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_b.csv'))
self.client = modbus_client.register_node_list(self.nodes)
self._modbus_client_raw = modbus_client
else:
print("测试模式,跳过连接")
self.nodes, self.client = None, None
self._modbus_client_raw = None
""" 工站的配置 """
@@ -195,40 +191,9 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self.csv_export_file = None
self.coin_num_N = 0 #已组装电池数量
def _ensure_modbus_connected(self) -> None:
"""检查 Modbus TCP 连接是否存活,若已断开则自动重连(防止长时间空闲后连接超时)"""
if self.debug_mode or self._modbus_client_raw is None:
return
raw_client = self._modbus_client_raw.client
if raw_client.is_socket_open():
return
logger.warning("[Modbus] 检测到连接已断开,尝试重连...")
try:
raw_client.close()
except Exception:
pass
count = 10
while count > 0:
count -= 1
try:
raw_client.connect()
except Exception:
pass
if raw_client.is_socket_open():
break
time.sleep(2)
if not raw_client.is_socket_open():
raise RuntimeError(f"Modbus TCP 重连失败({self._modbus_address}:{self._modbus_port}),请检查设备连接")
logger.info("[Modbus] 重连成功")
def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node
# Deck 为空时(反序列化未恢复子节点),主动调用 setup() 初始化子物料
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
logger.info("YihuaCoinCellDeck 无子节点,调用 setup() 初始化")
self.deck.setup()
#self.deck = create_a_coin_cell_deck()
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})
@@ -658,28 +623,12 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
return vol
@property
def data_coin_type(self) -> int:
"""电池类型 - 7种或8种组装物料 (INT16)"""
if self.debug_mode:
return 7
coin_type, read_err = self.client.use_node('REG_DATA_COIN_TYPE').read(1)
return coin_type
@property
def data_current_assembling_count(self) -> int:
"""当前进行组装的电池数量 - Current assembling battery count (INT16)"""
def data_coin_num(self) -> int:
"""当前电池数量 (INT16)"""
if self.debug_mode:
return 0
count, read_err = self.client.use_node('REG_DATA_CURRENT_ASSEMBLING_COUNT').read(1)
return count
@property
def data_current_completed_count(self) -> int:
"""当前完成组装的电池数量 - Current completed battery count (INT16)"""
if self.debug_mode:
return 0
count, read_err = self.client.use_node('REG_DATA_CURRENT_COMPLETED_COUNT').read(1)
return count
num, read_err = self.client.use_node('REG_DATA_COIN_NUM').read(1)
return num
@property
def data_coin_cell_code(self) -> str:
@@ -777,116 +726,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_10mm_positive_plate_remaining(self) -> float:
"""10mm正极片剩余物料数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_10MM_POSITIVE_PLATE_REMAINING_COUNT').address, count=2)
if result.isError():
logger.error("读取10mm正极片余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_12mm_positive_plate_remaining(self) -> float:
"""12mm正极片剩余物料数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_12MM_POSITIVE_PLATE_REMAINING_COUNT').address, count=2)
if result.isError():
logger.error("读取12mm正极片余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_16mm_positive_plate_remaining(self) -> float:
"""16mm正极片剩余物料数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_16MM_POSITIVE_PLATE_REMAINING_COUNT').address, count=2)
if result.isError():
logger.error("读取16mm正极片余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_aluminum_foil_remaining(self) -> float:
"""铝箔剩余物料数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_ALUMINUM_FOIL_REMAINING_COUNT').address, count=2)
if result.isError():
logger.error("读取铝箔余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_positive_shell_remaining(self) -> float:
"""正极壳剩余物料数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_POSITIVE_SHELL_REMAINING_COUNT').address, count=2)
if result.isError():
logger.error("读取正极壳余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_flat_washer_remaining(self) -> float:
"""平垫剩余物料数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_FLAT_WASHER_REMAINING_COUNT').address, count=2)
if result.isError():
logger.error("读取平垫余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_negative_shell_remaining(self) -> float:
"""负极壳剩余物料数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_NEGATIVE_SHELL_REMAINING_COUNT').address, count=2)
if result.isError():
logger.error("读取负极壳余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_spring_washer_remaining(self) -> float:
"""弹垫剩余物料数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_SPRING_WASHER_REMAINING_COUNT').address, count=2)
if result.isError():
logger.error("读取弹垫余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_finished_battery_remaining_capacity(self) -> float:
"""成品电池剩余可容纳数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_FINISHED_BATTERY_REMAINING_CAPACITY').address, count=2)
if result.isError():
logger.error("读取成品电池余量失败")
return 0.0
return _decode_float32_correct(result.registers)
@property
def data_finished_battery_ng_remaining_capacity(self) -> float:
"""成品电池NG槽剩余可容纳数量 (FLOAT32)"""
if self.debug_mode:
return 0.0
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_FINISHED_BATTERY_NG_REMAINING_CAPACITY').address, count=2)
if result.isError():
logger.error("读取成品电池NG槽余量失败")
return 0.0
return _decode_float32_correct(result.registers)
# @property
# def data_stack_vision_code(self) -> int:
# """物料堆叠复检图片编码 (INT16)"""
@@ -1086,7 +925,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# 步骤0: 前置条件检查
logger.info("\n【步骤 0/4】前置条件检查...")
self._ensure_modbus_connected()
try:
# 检查 REG_UNILAB_INTERACT (应该为False表示使用Unilab交互)
unilab_interact_node = self.client.use_node('REG_UNILAB_INTERACT')
@@ -1147,42 +985,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
raise RuntimeError(error_msg)
logger.info(" ✓ COIL_GB_L_IGNORE_CMD 检查通过 (值为False使用左手套箱)")
# 检查握手寄存器残留正常初始状态均应为False
# 若上次运行意外断网这些Unilab侧COIL可能被遗留为True导致PLC逻辑卡死
handshake_checks = [
("COIL_UNILAB_SEND_MSG_SUCC_CMD", "Unilab→PLC 配方发送完毕", "上次配方握手未正常复位PLC可能处于等待配方的卡死状态"),
("COIL_UNILAB_REC_MSG_SUCC_CMD", "Unilab→PLC 数据接收完毕", "上次数据接收握手未正常复位"),
("UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM", "Unilab→PLC 瓶数发送完毕", "上次瓶数握手未正常复位"),
("UNILAB_SEND_FINISHED_CMD", "Unilab→PLC 一组完成确认", "上次完成握手未正常复位"),
("COIL_REQUEST_REC_MSG_STATUS", "PLC→Unilab 请求接收配方", "PLC正处于等待配方状态设备流程已卡死需重启PLC或手动复位握手"),
("COIL_REQUEST_SEND_MSG_STATUS", "PLC→Unilab 请求发送测试数据", "PLC正处于等待发送数据状态设备流程已卡死"),
]
for coil_name, coil_desc, stuck_reason in handshake_checks:
try:
hs_node = self.client.use_node(coil_name)
hs_value, hs_err = hs_node.read(1)
if hs_err:
logger.warning(f" ⚠ 无法读取 {coil_name},跳过此项检查")
continue
hs_actual = hs_value[0] if isinstance(hs_value, (list, tuple)) else hs_value
logger.info(f" {coil_name} 当前值: {hs_actual}")
if hs_actual:
error_msg = (
"❌ 前置握手寄存器检查失败!\n"
f" {coil_name} = True (期望值: False)\n"
f" 含义: {coil_desc}\n"
f" 原因: {stuck_reason}\n"
" 建议: 检查上次运行是否意外中断手动将该寄存器置为False后重试"
)
logger.error(error_msg)
raise RuntimeError(error_msg)
logger.info(f"{coil_name} 检查通过 (值为False)")
except RuntimeError:
raise
except Exception as hs_e:
logger.warning(f" ⚠ 检查 {coil_name} 时发生异常: {hs_e},跳过此项")
logger.info("✓ 所有前置条件检查通过!")
except ValueError as e:
@@ -1356,8 +1158,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
lvbodian: bool = True,
battery_pressure_mode: bool = True,
battery_clean_ignore: bool = False,
file_path: str = "/Users/sml/work",
formulations: List[Dict] = None
file_path: str = "/Users/sml/work"
) -> Dict[str, Any]:
"""
发送瓶数+简化组装函数(适用于第二批次及后续批次)
@@ -1384,77 +1185,17 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
battery_pressure_mode: 是否启用压力模式
battery_clean_ignore: 是否忽略电池清洁
file_path: 实验记录保存路径
formulations: 配方信息列表(从 create_orders.mass_ratios 获取)
包含 orderCode, target_mass_ratio, real_mass_ratio 等
用于CSV数据追溯可选参数
Returns:
dict: 包含组装结果的字典
注意:
注意
- 第一次启动需先调用 func_pack_device_init_auto_start_combined()
- 后续批次直接调用此函数即可
"""
logger.info("=" * 60)
logger.info("开始发送瓶数+简化组装流程...")
logger.info(f"电解液瓶数: {elec_num}, 每瓶电池数: {elec_use_num}")
# 存储配方信息到设备状态(供 CSV 写入使用)
if formulations:
logger.info(f"接收到配方信息: {len(formulations)}")
# 将配方信息按 orderCode 索引,方便后续查找
self._formulations_map = {
f["orderCode"]: f for f in formulations
} if formulations else {}
# ✅ 新增:存储配方列表(按接收顺序),用于索引访问(兜底用)
self._formulations_list = formulations
# ✅ 新增按分液瓶条码vial_bottle_barcodes反向索引配方
# 配液站夹爪取放顺序与扣电站夹取顺序可能不同,所以不能再依赖位置序号,
# 必须用扣电站扫码得到的 data_electrolyte_code 去对齐配液站登记的瓶条码。
# vial_bottle_barcodes 字段可能形如 "LG100114"(单瓶)或 '["LG100114","LG100115"]'(多瓶)。
self._formulations_by_vial_barcode: Dict[str, Dict] = {}
for f in formulations:
raw_barcodes = f.get("vial_bottle_barcodes", "")
if not raw_barcodes:
continue
barcodes: List[str] = []
if isinstance(raw_barcodes, list):
barcodes = [str(b).strip() for b in raw_barcodes if b]
else:
s = str(raw_barcodes).strip()
if s.startswith("[") and s.endswith("]"):
try:
parsed = json.loads(s)
if isinstance(parsed, list):
barcodes = [str(b).strip() for b in parsed if b]
else:
barcodes = [str(parsed).strip()]
except Exception:
barcodes = [s]
else:
barcodes = [s]
for bc in barcodes:
if bc and bc not in self._formulations_by_vial_barcode:
self._formulations_by_vial_barcode[bc] = f
logger.info(
f"已建立分液瓶条码 → 配方索引: {len(self._formulations_by_vial_barcode)}"
f"(条码: {list(self._formulations_by_vial_barcode.keys())})"
)
else:
logger.warning("未接收到配方信息CSV将不包含配方字段")
self._formulations_map = {}
self._formulations_list = []
self._formulations_by_vial_barcode = {}
# ✅ 新增:存储每瓶电池数,用于计算当前使用的瓶号
# ⚠️ 确保转换为整数(前端可能传递字符串)
self._elec_use_num = int(elec_use_num) if elec_use_num else 0
logger.info(f"已存储参数: 每瓶电池数={self._elec_use_num}, 配方数={len(self._formulations_list)}")
# ✅ 新增:软件层电池计数器(防止硬件计数器不准确)
self._software_battery_counter = 0 # 从0开始每写入一次CSV递增
logger.info("软件层电池计数器已初始化")
logger.info("=" * 60)
# 步骤1: 发送电解液瓶数(触发物料搬运)
@@ -1590,8 +1331,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
data_assembly_time = self.data_assembly_time
data_assembly_pressure = self.data_assembly_pressure
data_electrolyte_volume = self.data_electrolyte_volume
data_coin_type = self.data_coin_type # 电池类型7或8种物料
data_battery_number = self.data_current_assembling_count # ✅ 真正的电池编号
data_coin_num = self.data_coin_num
# 处理电解液二维码 - 确保是字符串类型
try:
@@ -1621,32 +1361,28 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
logger.debug(f"data_assembly_time: {data_assembly_time}")
logger.debug(f"data_assembly_pressure: {data_assembly_pressure}")
logger.debug(f"data_electrolyte_volume: {data_electrolyte_volume}")
logger.debug(f"data_coin_type: {data_coin_type}") # 电池类型
logger.debug(f"data_battery_number: {data_battery_number}") # ✅ 电池编号
logger.debug(f"data_coin_num: {data_coin_num}")
logger.debug(f"data_electrolyte_code: {data_electrolyte_code}")
logger.debug(f"data_coin_cell_code: {data_coin_cell_code}")
#接收完信息后读取完毕标志位置True
finished_battery_magazine = self.deck.get_resource("成品弹夹")
# 计算电池应该放在哪个洞,以及洞内的堆叠位置
# 成品弹夹有6个洞每个洞可堆叠20颗电池
# 前5个洞索引0-4放正常电池第6个洞索引5放NG电池
BATTERIES_PER_HOLE = 20
MAX_NORMAL_BATTERIES = 100 # 5个洞 × 20颗/洞
hole_index = self.coin_num_N // BATTERIES_PER_HOLE # 第几个洞0-4为正常电池
in_hole_position = self.coin_num_N % BATTERIES_PER_HOLE # 洞内的堆叠序号
if hole_index >= 5:
logger.error(f"电池数量超出正常容量范围: {self.coin_num_N + 1} > {MAX_NORMAL_BATTERIES}")
raise ValueError(f"成品弹夹正常洞位已满(最多{MAX_NORMAL_BATTERIES}颗),当前尝试放置第{self.coin_num_N + 1}")
target_hole = finished_battery_magazine.children[hole_index] # 获取目标洞
liaopan3 = self.deck.get_resource("成品弹夹")
# 生成唯一的电池名称(使用时间戳确保唯一性)
timestamp_suffix = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
battery_name = f"battery_{self.coin_num_N}_{timestamp_suffix}"
# 检查目标位置是否已有资源,如果有则先卸载
target_slot = liaopan3.children[self.coin_num_N]
if target_slot.children:
logger.warning(f"位置 {self.coin_num_N} 已有资源,将先卸载旧资源")
try:
# 卸载所有现有子资源
for child in list(target_slot.children):
target_slot.unassign_child_resource(child)
logger.info(f"已卸载旧资源: {child.name}")
except Exception as e:
logger.error(f"卸载旧资源时出错: {e}")
# 创建新的电池资源
battery = ElectrodeSheet(name=battery_name, size_x=14, size_y=14, size_z=2)
battery._unilabos_state = {
@@ -1657,12 +1393,13 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"electrolyte_volume": data_electrolyte_volume
}
# 将电池堆叠到目标洞中
# 分配新资源到目标位置
try:
target_hole.assign_child_resource(battery, location=None)
logger.info(f"成功放置电池 {battery_name}弹夹洞{hole_index}的第{in_hole_position + 1}层 (总计第{self.coin_num_N + 1}颗)")
target_slot.assign_child_resource(battery, location=None)
logger.info(f"成功分配电池 {battery_name}位置 {self.coin_num_N}")
except Exception as e:
logger.error(f"放置电池资源失败: {e}")
logger.error(f"分配电池资源失败: {e}")
# 如果分配失败,尝试使用更简单的方法
raise
#print(jipian2.parent)
@@ -1683,7 +1420,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
time_date = datetime.now().strftime("%Y%m%d")
#秒级时间戳用于标记每一行电池数据
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self._last_assembly_timestamp = timestamp
#生成输出文件的变量
self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv")
#将数据存入csv文件
@@ -1694,107 +1430,17 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
writer.writerow([
'Time', 'open_circuit_voltage', 'pole_weight',
'assembly_time', 'assembly_pressure', 'electrolyte_volume',
'data_coin_type', 'electrolyte_code', 'coin_cell_code',
'orderName', 'prep_bottle_barcode', 'vial_bottle_barcodes',
'target_mass_ratio', 'real_mass_ratio'
'coin_num', 'electrolyte_code', 'coin_cell_code'
])
#立刻写入磁盘
csvfile.flush()
#开始追加电池信息
with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
# ========== 提取配方信息 ==========
formulation_order_name = ""
prep_bottle_barcode = ""
vial_bottle_barcodes = ""
target_ratio_str = ""
real_ratio_str = ""
# 从 self._formulations_list 获取配方信息
if hasattr(self, '_formulations_list') and self._formulations_list:
# ============================================================
# ✅ 主方案:用扣电站扫码得到的电解液瓶条码 (data_electrolyte_code)
# 反查配液站登记的 vial_bottle_barcodes避免依赖夹爪取放顺序。
# 配液站和扣电站的瓶子顺序往往不一致(不同自动化设备的取放策略不同),
# 按位置序号匹配会错位;但每个瓶子的条码是唯一的,按条码匹配最可靠。
# ============================================================
formulation = None
match_method = ""
barcode_map = getattr(self, "_formulations_by_vial_barcode", {}) or {}
scan_code = (data_electrolyte_code or "").strip()
if scan_code and scan_code != "N/A" and barcode_map:
formulation = barcode_map.get(scan_code)
if formulation is not None:
match_method = f"按条码({scan_code})精确匹配"
else:
logger.warning(
f"[CSV写入] 电池 {data_battery_number}: 扫码条码 {scan_code} "
f"在配方索引中找不到 (已登记条码: {list(barcode_map.keys())})"
)
# ============================================================
# 🔁 降级方案:扫码失败 / 条码缺失时按瓶号位置兜底
# 保留原有"每瓶电池数"或"二维码尾号"的位置推断逻辑,
# 确保在异常路径下仍能落盘(位置推断的结果可能不准,仅供回溯)。
# ============================================================
if formulation is None:
if hasattr(self, '_elec_use_num') and self._elec_use_num:
elec_use_num_int = int(self._elec_use_num) if self._elec_use_num else 1
if elec_use_num_int > 0:
current_bottle_index = (data_battery_number - 1) // elec_use_num_int
else:
current_bottle_index = 0
logger.debug(
f"[CSV写入] 电池 {data_battery_number}: 降级按瓶号索引={current_bottle_index} "
f"(每瓶{self._elec_use_num}颗电池)"
)
else:
current_bottle_index = (
int(data_electrolyte_code.split('-')[-1])
if '-' in str(data_electrolyte_code)
else 0
)
logger.debug(
f"[CSV写入] 电池 {data_battery_number}: 降级按二维码尾号瓶号索引={current_bottle_index}"
)
if 0 <= current_bottle_index < len(self._formulations_list):
formulation = self._formulations_list[current_bottle_index]
match_method = f"按位置兜底匹配[{current_bottle_index}]"
else:
logger.warning(
f"[CSV写入] 电池 {data_battery_number}: 瓶号索引 {current_bottle_index} "
f"超出配方列表范围 (共{len(self._formulations_list)}个配方)"
)
if formulation is not None:
formulation_order_name = formulation.get("orderName", "")
prep_bottle_barcode = formulation.get("prep_bottle_barcode", "")
vial_bottle_barcodes = formulation.get("vial_bottle_barcodes", "")
real_ratio = formulation.get("real_mass_ratio", {})
target_ratio = formulation.get("target_mass_ratio", {})
target_ratio_str = json.dumps(target_ratio, ensure_ascii=False) if target_ratio else ""
real_ratio_str = json.dumps(real_ratio, ensure_ascii=False) if real_ratio else ""
logger.info(
f"[CSV写入] 电池 {data_battery_number} ({match_method}): "
f"orderName={formulation_order_name}, 配液瓶={prep_bottle_barcode}, "
f"分液瓶={vial_bottle_barcodes}"
)
else:
logger.debug(f"[CSV写入] 电池 {data_battery_number}: 未找到配方信息数据")
writer.writerow([
timestamp, data_open_circuit_voltage, data_pole_weight,
data_assembly_time, data_assembly_pressure, data_electrolyte_volume,
data_coin_type, data_electrolyte_code, data_coin_cell_code,
formulation_order_name, prep_bottle_barcode, vial_bottle_barcodes,
target_ratio_str, real_ratio_str
data_coin_num, data_electrolyte_code, data_coin_cell_code
])
#立刻写入磁盘
csvfile.flush()
@@ -1939,18 +1585,17 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
pole_weight = 0.0
battery_info = {
"Time": getattr(self, "_last_assembly_timestamp", datetime.now().strftime("%Y%m%d_%H%M%S")),
"battery_index": coin_num_N + 1,
"battery_barcode": battery_qr_code,
"electrolyte_barcode": electrolyte_qr_code,
"open_circuit_voltage": open_circuit_voltage,
"pole_weight": pole_weight,
"assembly_time": self.data_assembly_time,
"assembly_pressure": self.data_assembly_pressure,
"electrolyte_volume": self.data_electrolyte_volume,
"data_coin_type": getattr(self, "data_coin_type", 0),
"electrolyte_code": electrolyte_qr_code,
"coin_cell_code": battery_qr_code,
"electrolyte_volume": self.data_electrolyte_volume
}
battery_data_list.append(battery_info)
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['coin_cell_code']}, 电解液码={battery_info['electrolyte_code']}")
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['battery_barcode']}, 电解液码={battery_info['electrolyte_barcode']}")
time.sleep(1)
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
@@ -1979,7 +1624,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"success": True,
"total_batteries": len(battery_data_list),
"batteries": battery_data_list,
"assembly_data": battery_data_list,
"summary": {
"electrolyte_bottles_used": elec_num,
"batteries_per_bottle": elec_use_num,
@@ -2023,7 +1667,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
file_path: str = "/Users/sml/work"
) -> Dict[str, Any]:
"""
简化版电池组装函数,整合了原 qiming_coin_cell_code 的参数设置和双滴模式
此函数是 func_allpack_cmd 的增强版本,自动处理以下配置:
- 负极片和隔膜的盘数及矩阵点位
- 枪头盒矩阵点位
@@ -2194,18 +1839,17 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
pole_weight = 0.0
battery_info = {
"Time": getattr(self, "_last_assembly_timestamp", datetime.now().strftime("%Y%m%d_%H%M%S")),
"battery_index": coin_num_N + 1,
"battery_barcode": battery_qr_code,
"electrolyte_barcode": electrolyte_qr_code,
"open_circuit_voltage": open_circuit_voltage,
"pole_weight": pole_weight,
"assembly_time": self.data_assembly_time,
"assembly_pressure": self.data_assembly_pressure,
"electrolyte_volume": self.data_electrolyte_volume,
"data_coin_type": getattr(self, "data_coin_type", 0),
"electrolyte_code": electrolyte_qr_code,
"coin_cell_code": battery_qr_code,
"electrolyte_volume": self.data_electrolyte_volume
}
battery_data_list.append(battery_info)
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['coin_cell_code']}, 电解液码={battery_info['electrolyte_code']}")
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['battery_barcode']}, 电解液码={battery_info['electrolyte_barcode']}")
time.sleep(1)
@@ -2232,7 +1876,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"success": True,
"total_batteries": len(battery_data_list),
"batteries": battery_data_list,
"assembly_data": battery_data_list,
"summary": {
"electrolyte_bottles_used": elec_num,
"batteries_per_bottle": elec_use_num,
@@ -2279,7 +1922,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
def fun_wuliao_test(self) -> bool:
#找到data_init中构建的2个物料盘
test_battery_plate = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
for i in range(16):
battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2)
battery._unilabos_state = {
@@ -2289,7 +1932,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"electrolyte_volume": 20.0,
"electrolyte_name": f"DP{i}"
}
test_battery_plate.children[i].assign_child_resource(battery, location=None)
liaopan3.children[i].assign_child_resource(battery, location=None)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
@@ -2332,7 +1975,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
data_assembly_time = self.data_assembly_time
data_assembly_pressure = self.data_assembly_pressure
data_electrolyte_volume = self.data_electrolyte_volume
data_coin_type = self.data_coin_type # 电池类型7或8种物料
data_coin_num = self.data_coin_num
data_electrolyte_code = self.data_electrolyte_code
data_coin_cell_code = self.data_coin_cell_code
# 电解液瓶位置
@@ -2446,7 +2089,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
writer.writerow([
timestamp, data_open_circuit_voltage, data_pole_weight,
data_assembly_time, data_assembly_pressure, data_electrolyte_volume,
data_coin_type, data_electrolyte_code, data_coin_cell_code # ✅ 已修正
data_coin_num, data_electrolyte_code, data_coin_cell_code
])
#立刻写入磁盘
csvfile.flush()
@@ -2497,7 +2140,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
if __name__ == "__main__":
# 简单测试
workstation = CoinCellAssemblyWorkstation(deck=yihua_coin_cell_deck(name="coin_cell_deck"))
workstation = CoinCellAssemblyWorkstation(deck=CoincellDeck(setup=True, name="coin_cell_deck"))
# workstation.qiming_coin_cell_code(fujipian_panshu=1, fujipian_juzhendianwei=2, gemopanshu=3, gemo_juzhendianwei=4, lvbodian=False, battery_pressure_mode=False, battery_pressure=4200, battery_clean_ignore=False)
# print(f"工作站创建成功: {workstation.deck.name}")
# print(f"料盘数量: {len(workstation.deck.children)}")

View File

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

View File

@@ -1,88 +0,0 @@
# 物料系统标准化重构方案
根据开发者的反馈本方案旨在遵循“标准化而非绕过”的原则对资源类Deck、Carrier、Magazine进行重构。核心目标是将物理结构的初始化与物料/极片的初始填充逻辑解耦,彻底解决反序列化过程中的初始化冲突。
## 拟议变更
### [参考] PRCXI9300 标准化模式
#### [参考文件] [prcxi.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/liquid_handling/prcxi/prcxi.py)
* **PRCXI9300Deck**: 演示了如何在 `serialize` 中导出 `sites` 元数据,以及如何在 `assign_child_resource` 中实现稳健的槽位匹配(支持按名称、坐标或索引匹配)。
* **PRCXI9300Container**: 演示了标准的 `load_state``serialize_state` 模式,确保业务状态(如 `Material` UUID能正确往返序列化。
### [组件] 台面 (Decks)
#### [修改] [decks.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/bioyond/decks.py)
*`BIOYOND_YB_Deck` 重命名为 **`BioyondElectrolyteDeck`**,对应工厂函数 `YB_Deck()` 重命名为 **`bioyond_electrolyte_deck()`**。
* `BIOYOND_PolymerReactionStation_Deck``BIOYOND_PolymerPreparationStation_Deck` **保持不变**
* 以上三个 Deck 的 `__init__` 中均移除 `setup` 参数和 `setup()` 调用,删除临时的 `deserialize` 重写。
#### [修改 + 重命名] [YB_YH_materials.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py) → `yihua_coin_cell_materials.py`
*`CoincellDeck` 重命名为 **`YihuaCoinCellDeck`**,对应工厂函数 `YH_Deck()` 重命名为 **`yihua_coin_cell_deck()`**。
*`YihuaCoinCellDeck.__init__` 中移除 `setup` 参数和 `setup()` 调用,删除临时的 `deserialize` 重写。
### [组件] 容器类与弹夹 (Itemized Carriers & Magazines)
#### [修改] [magazine.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/battery/magazine.py)
* 重构 `magazine_factory`:将创建 `MagazineHolder` 几何结构(空槽位)的过程与填充 `ElectrodeSheet` 物料的过程分离。
* 确保 `MagazineHolder``Magazine``__init__` 过程中不主动创建任何内容物。
#### [修改] [warehouse.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/warehouse.py)
* 确保 `WareHouse` 类和 `warehouse_factory` 遵循相同模式:先初始化几何结构,内容物另行处理。
#### [修改] [itemized_carrier.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/itemized_carrier.py)
* 移除之前添加的 `idx is None` 兜底补丁。
* 修复命名规范,确保 `assign_child_resource` 在反序列化时能准确匹配资源。
### [组件] 状态兼容性 (State Compatibility)
#### [修改] [resource_tracker.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/resource_tracker.py)
*`to_plr_resources` 方法中调用 `load_all_state` 之前,预处理 `all_states` 字典。
* 对于 `Container` 类型的资源,如果其状态中缺少 `liquid_history``pending_liquids` 等 PLR 新版本要求的键,则填充默认值(如空列表/字典),防止反序列化中断。
### [组件] 料盘 (Material Plates)
#### [修改] [YB_YH_materials.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py)
* 重构 `MaterialPlate`:不在 `__init__` 中直接调用 `create_ordered_items_2d`
* 重构 `YIHUA_Electrolyte_12VialCarrier`:将其修改为标准的基类定义或在工厂方法中彻底剥离内部 12 个 `YB_pei_ye_xiao_Bottle` 的强制初始化,以防反序列化冲突。
### [组件] 跨站转运与分液瓶板 (Vial Plate Transfer)
#### [修改] [bioyond_cell_workstation.py] & [YB_YH_materials.py]
* **分析**:目前的 `bioyond_cell_workstation.py` 在执行转移时,是用 `sites=["electrolyte_buffer"]` 试图把整块 `YB_Vial_5mL_Carrier` 板转移给目标。但由于实际工艺中,配液站将分液瓶板传往扣电工站后,是由扣电工站的机械臂**逐瓶抓取**并放入内部的 `bottle_rack_6x2`(电解液缓存位),用完后再放入 `bottle_rack_6x2_2`(废液位),因此配液站的这一次“跨工位资源树转移”在逻辑上存在偏差:目标槽位不应该是装单瓶的载体 `bottle_rack`
* **修复方案**
1. **目标端 (Yihua 侧)**
*`YB_YH_materials.py` 中为从配液站传过来的“分液瓶板”本身设置一个接驳专用的 `PlateSlot`(或者单纯直接移到 Deck 指定坐标)。这个位置负责真正在资源树层级上合法接收配液站传过来的完整 Board。
* 重构 `YIHUA_Electrolyte_12VialCarrier`:为了防止初始化反序列化冲突,取消内部在 `__init__` 中自动填充满 12 个 `YB_pei_ye_xiao_Bottle` 实例的逻辑。`bottle_rack_6x2``bottle_rack_6x2_2` 初始化时均应为空。
2. **转运端 (Bioyond 侧)**
* 修改 `bioyond_cell_workstation.py` 的资源树数字转运代码,将其转移目标对应到 Yihua 侧新设立的“分液瓶板接驳区域”资源,或者干脆只更新资源树坐标位置(使其脱离 Bioyond Deck 加入 Yihua Deck而不再强行挂载到一个无法容纳 Carrier 的 `bottle_rack_6x2` 内部。
### [组件] 依华扣电组装工站物料余量监控 (Material Monitoring)
#### [修改] 寄存器直读与前端集成
* **物理对象保留但虚化追踪**:原有的实体台面对象(如 `MaterialPlate``MagazineHolder` 各类型及其对应的洞位坐标)**仍然保留并使用**。保留它们是为了给机器臂提供基础的物理空间取放标定,以及作为前端页面的可视和可交互区块。
* **内部物料免追踪**:既然余量完全由寄存器接管,**我们将不再在这些弹夹或洞位内部显式生成、塞入和追踪每一个具体的极片或外壳对象 (如 `ElectrodeSheet` 等)**。这恰好与我们的重构主旨(不主动在 `__init__` 建子物料以避开反序列化冲突)完美结合,进一步极大地减轻了后台资源树对象的复杂度。
* **监控方式变更**:放弃现有的物料余量方式,直接读取依华扣电组装工站开放的寄存器地址以获取准确余量。
* **前端界面集成**:在前端界面点击负极壳、弹垫片等弹夹的 data view 时,直接读取并显示寄存器中的各自余量。
* **新增寄存器映射** (参考 `coin_cell_assembly_b.csv`)
* `10mm正极片剩余物料数量R``read hold_register 520` (REAL)
* `12mm正极片剩余物料数量R``read hold_register 522` (REAL)
* `16mm正极片剩余物料数量R``read hold_register 524` (REAL)
* `铝箔剩余物料数量R``read hold_register 526` (REAL)
* `正极壳剩余物料数量R``read hold_register 528` (REAL)
* `平垫剩余物料数量R``read hold_register 530` (REAL)
* `负极壳剩余物料数量R``read hold_register 532` (REAL)
* `弹垫剩余物料数量R``read hold_register 534` (REAL)
* `成品电池剩余可容纳数量R``read hold_register 536` (REAL)
* `成品电池NG槽剩余可容纳数量R``read hold_register 538` (REAL)
### [配置] JSON 配置文件 (Configuration Files)
#### [修改] 资源类型名称更新
* 更新以下配置文件,将其中的 `BIOYOND_YB_Deck` 替换为新的类名 **`BioyondElectrolyteDeck`**,以及将 `coin_cell_deck` 替换为 **`YihuaCoinCellDeck`**
* `yibin_electrolyte_config.json`
* `yibin_coin_cell_only_config.json`
* `yibin_electrolyte_only_config.json`
## 验证计划
### 自动化测试
* 对重构后的类运行 `pylabrobot` 序列化/反序列化测试,确保状态能够完美恢复。
* 检查各工作站节点启动时是否仍存在 `ValueError: Resource '...' already assigned to deck` 报错。
* 检查 `resource_tracker` 中是否仍存在重复 UUID 报错。
### 手动验证
* 重启各工作站节点,验证资源树是否能根据数据库数据正确还还原。
* 验证“自动”与“手动”传输窗资源在台面上的分配是否正确。

View File

@@ -1,388 +0,0 @@
# 物料系统标准化重构方案 v2增强版
> **基于原始方案 (`implementation_plan.md`) 的补充与细化**。
> 本文档在原方案基础上:①增加当前代码现状核查结果;②明确各任务的执行顺序与文件级改动;③新增注意事项与回归测试命令。
---
## 0. 核心原则(保持不变)
"**物理几何结构初始化Deck / Carrier / Magazine 的 `__init__`)与物料内容物填充(`setup()` / `klasses` 参数)必须彻底解耦**",以消除 PLR 反序列化时的 `Resource already assigned to deck` 错误。
---
## 1. 当前代码现状核查2026-03-12
| 文件 | 计划要求 | 当前状态 | 是否完成 |
|---|---|---|---|
| `resources/bioyond/decks.py` | 重命名类;移除 `setup` 参数和 `deserialize` 补丁 | 仍是 `BIOYOND_YB_Deck``setup` 参数和 `deserialize` 均存在 | ❌ |
| `coin_cell_assembly/YB_YH_materials.py` | 重命名类;文件迁移;移除补丁 | 仍是 `CoincellDeck``setup` 参数和 `deserialize` 均存在 | ❌ |
| `resources/battery/magazine.py` | `magazine_factory` 不主动填充物料 | `MagazineHolder_6_Cathode` / `_6_Anode` / `_4_Cathode` 仍传 `klasses`,初始化时填满极片 | ❌ |
| `resources/battery/bottle_carriers.py` | `YIHUA_Electrolyte_12VialCarrier` 初始化时不填充瓶子 | 第 54-55 行仍循环填充 12 个 `YB_pei_ye_xiao_Bottle` | ❌ |
| `resources/itemized_carrier.py` | 移除 `idx is None` 兜底补丁 | 第 182-190 行仍保留该兜底逻辑 | ❌(待前置任务完成后移除) |
| `resources/resource_tracker.py` | `load_all_state` 前预填 `Container` 缺失键 | 第 616 行直接调用,无预处理 | ❌ |
| `bioyond_cell_workstation.py` | 修正跨站转运目标为合法接驳槽 | 第 1563 行仍 `sites=["electrolyte_buffer"]`,目标 UUID 为硬编码虚拟资源 | ❌ |
| `yibin_*.json` 配置文件 | 更新类名 | 仍使用 `BIOYOND_YB_Deck` / `CoincellDeck` | ❌ |
| `registry/resources/bioyond/deck.yaml` | 更新类名(原计划未提及) | 仍使用 `BIOYOND_YB_Deck` / `CoincellDeck` | ❌(**新增** |
---
## 2. 执行顺序(含依赖关系)
```
阶段 A底层资源类
A1. magazine.py — 移除 klasses 填充
A2. bottle_carriers.py — 移除瓶子填充
阶段 BDeck 层)
B1. decks.py — 移除 setup 参数和 deserialize 补丁;重命名
B2. YB_YH_materials.py → 重命名;移除 CoincellDeck 的 setup 参数和 deserialize 补丁
阶段 C状态兼容
C1. resource_tracker.py — 预填 Container 缺失键
C2. itemized_carrier.py — 移除 idx is None 兜底补丁B 阶段完成后)
阶段 D跨站转运修复
D1. YB_YH_materials.py 新增 vial_plate_dock接驳专用槽
D2. bioyond_cell_workstation.py 修正 transfer 目标
阶段 E配置与注册表
E1. yibin_*.json 更新类名
E2. registry/resources/bioyond/deck.yaml 更新类名
E3. coin_cell_assembly.py 更新导入路径(若文件重命名)
```
---
## 3. 分阶段详细说明
---
### 阶段 A — 底层资源类
#### A1. `unilabos/resources/battery/magazine.py`
**问题**`MagazineHolder_6_Cathode``MagazineHolder_6_Anode``MagazineHolder_4_Cathode` 在调用 `magazine_factory` 时传入 `klasses`,导致每次 `__init__` 就填满极片,反序列化时重复添加。
**修改**
- 将三个函数中的 `klasses=[...]` 改为 `klasses=None`(与 `MagazineHolder_6_Battery` 保持一致)。
- **理由**:物料余量已由寄存器管理(见阶段 F不需要在资源树中追踪每一个极片。
```python
# 修改前MagazineHolder_6_Cathode 举例)
klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan],
# 修改后
klasses=None,
```
> **注意**`magazine_factory` 中 `klasses` 参数及循环体代码保留(仍可按需在非序列化场景使用),只是各具体工厂函数不再传入。
---
#### A2. `unilabos/resources/battery/bottle_carriers.py`
**问题**`YIHUA_Electrolyte_12VialCarrier` 第 54-55 行在工厂函数末尾循环填充 12 个瓶子。
**修改**:删除以下两行:
```python
# 删除
for i in range(12):
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}")
```
**理由**`bottle_rack_6x2``bottle_rack_6x2_2` 均应初始化为空,瓶子由 Bioyond 侧实际转运后再填入。
---
### 阶段 B — Deck 层重构
#### B1. `unilabos/resources/bioyond/decks.py`
**改动列表**
1. **重命名** `BIOYOND_YB_Deck``BioyondElectrolyteDeck`
2. **重命名** `YB_Deck()` 工厂函数 → `bioyond_electrolyte_deck()`
3. **移除** `__init__` 中的 `setup: bool = False` 参数及 `if setup: self.setup()` 调用
4. **删除** `deserialize` 方法重写(该临时补丁在 `setup` 参数移除后自然失效,继续保留反而掩盖问题)
5. `BIOYOND_PolymerReactionStation_Deck``BIOYOND_PolymerPreparationStation_Deck` 同步执行第 3、4 步
**重构后初始化模式**
```python
class BioyondElectrolyteDeck(Deck):
def __init__(self, name: str = "YB_Deck", ...):
super().__init__(name=name, ...)
# ❌ 不调用 self.setup()
# PLR 反序列化时只会调用 __init__然后从 children JSON 重建子资源
def setup(self) -> None:
# 完整的子资源初始化逻辑保留在这里,只由工厂函数调用
...
def bioyond_electrolyte_deck(name: str) -> BioyondElectrolyteDeck:
deck = BioyondElectrolyteDeck(name=name)
deck.setup() # ✅ 工厂函数负责填充
return deck
```
**同步修改**
- `bioyond_cell_workstation.py` 第 20 行:
```python
# 修改前
from unilabos.resources.bioyond.decks import BIOYOND_YB_Deck
# 修改后
from unilabos.resources.bioyond.decks import BioyondElectrolyteDeck
```
- 同文件第 2440 行:`BIOYOND_YB_Deck(setup=True)` → `bioyond_electrolyte_deck(name="YB_Deck")`
---
#### B2. `unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`
**改动列表**
1. **重命名** `CoincellDeck` → `YihuaCoinCellDeck`
2. **重命名** `YH_Deck()` → `yihua_coin_cell_deck()`(可保留 `YH_Deck` 作为兼容别名,日后废弃)
3. **移除** `CoincellDeck.__init__` 中 `setup: bool = False` 参数及调用
4. **删除** `CoincellDeck.deserialize` 重写方法
5. `MaterialPlate.__init__` 中移除 `fill` 参数,始终不主动调用 `create_ordered_items_2d`(当前 `fill=False` 路径已正确,只需删除 `fill=True` 分支)
```python
# 修改前MaterialPlate.__init__ 片段)
if fill:
super().__init__(..., ordered_items=holes, ...)
else:
super().__init__(..., ordered_items=ordered_items, ...)
# 修改后(始终走 "不填充" 路径)
super().__init__(..., ordered_items=ordered_items, ...)
# holes 的创建代码整体移入独立工厂方法
```
**同步修改**
- `coin_cell_assembly.py` 第 20 行导入:
```python
# 修改前
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
# 修改后
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import YihuaCoinCellDeck
```
- 同文件第 2245 行:`CoincellDeck(setup=True, name="coin_cell_deck")` → `yihua_coin_cell_deck(name="coin_cell_deck")`
- 文件重命名(可选):`YB_YH_materials.py` → `yihua_coin_cell_materials.py`(若重命名,所有 import 路径需全局替换)
---
### 阶段 C — 状态兼容
#### C1. `unilabos/resources/resource_tracker.py`
**问题**:第 616 行直接调用 `plr_resource.load_all_state(all_states)`,若 `Container` 类资源的 `data` 字段缺少 `liquid_history` 或 `pending_liquids`PLR 新版本会抛出 `KeyError`。
**修改**:在第 616 行前插入预处理:
```python
# 在 load_all_state 调用前预填缺失键
from pylabrobot.resources.container import Container as PLRContainer
for res_name, state in all_states.items():
if state and isinstance(state, dict):
# Container 类型要求这两个键存在
state.setdefault("liquid_history", [])
state.setdefault("pending_liquids", {})
plr_resource.load_all_state(all_states)
```
---
#### C2. `unilabos/resources/itemized_carrier.py`
**前提**B1、B2 阶段完成Deck 类名与资源命名规范已对齐后再执行此步。
**修改**:删除第 182-190 行的兜底补丁:
```python
# 删除以下整个 if 块
if idx is None:
fallback_location = location if location is not None else Coordinate.zero()
super().assign_child_resource(resource, location=fallback_location, reassign=reassign)
return
```
**替代**:改为抛出带诊断信息的异常,便于后续问题排查:
```python
if idx is None:
raise ValueError(
f"[ItemizedCarrier] 无法为资源 '{resource.name}' 找到匹配的槽位。"
f"已知槽位:{list(self.child_locations.keys())}"
f"传入坐标:{location}"
)
```
---
### 阶段 D — 跨站转运修复
#### D1. `YB_YH_materials.py` — 新增分液瓶板接驳槽
在 `YihuaCoinCellDeck.setup()` 中,新增一个专用于接收 Bioyond 侧传来的完整分液瓶板的 `ResourceStack`(或 `PlateSlot`
```python
# 在 setup() 末尾追加
from pylabrobot.resources.resource_stack import ResourceStack
vial_plate_dock = ResourceStack(
name="electrolyte_buffer", # 保持与 bioyond_cell_workstation.py 的 sites 键一致
direction="z",
resources=[],
)
self.assign_child_resource(vial_plate_dock, Coordinate(x=1050.0, y=700.0, z=0))
```
> **说明**:槽位命名 `electrolyte_buffer` 与 `bioyond_cell_workstation.py` 现有的 `sites=["electrolyte_buffer"]` 对应减少改动量。如改名D2 需同步。
---
#### D2. `bioyond_cell_workstation.py` — 修正 transfer 目标
**问题**:第 1545-1552 行创建了一个 `size=1,1,1` 的虚拟 `ResourcePLR` 并硬编码 UUID这个对象在 YihuaCoinCellDeck 的资源树中不存在,导致转移后资源树状态混乱。
**修改**
```python
# 修改前:创建虚拟目标资源
target_resource_obj = ResourcePLR(name=target_location, size_x=1.0, ...)
target_resource_obj.unilabos_uuid = "550e8400-e29b-41d4-a716-446655440001" # 硬编码
# 修改后:通过 ROS2/设备注册表查询真实资源
# (需要从 target_device 的资源树中取出 electrolyte_buffer 的真实对象)
target_resource_obj = self._get_resource_from_device(
device_id=target_device,
resource_name=target_location
)
if target_resource_obj is None:
raise RuntimeError(
f"目标设备 {target_device} 中未找到资源 '{target_location}'"
f"请确认 YihuaCoinCellDeck.setup() 中已添加 electrolyte_buffer 槽位"
)
```
> **说明**`_get_resource_from_device` 需根据现有 ROS2 资源同步机制实现,或复用已有的 `get_plr_resource_by_name` 类似方法。
---
### 阶段 E — 配置与注册表
#### E1. `yibin_electrolyte_config.json` / `yibin_coin_cell_only_config.json` / `yibin_electrolyte_only_config.json`
全局替换以下字符串:
| 旧值 | 新值 |
|---|---|
| `BIOYOND_YB_Deck` | `BioyondElectrolyteDeck` |
| `unilabos.resources.bioyond.decks:BIOYOND_YB_Deck` | `unilabos.resources.bioyond.decks:BioyondElectrolyteDeck` |
| `CoincellDeck` | `YihuaCoinCellDeck` |
| `unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck` | 若文件已重命名:`unilabos.devices.workstation.coin_cell_assembly.yihua_coin_cell_materials:YihuaCoinCellDeck` |
---
#### E2. `unilabos/registry/resources/bioyond/deck.yaml`**原计划未覆盖,新增**
当前第 25 行和第 37 行仍使用旧类名,需同步更新:
```yaml
# 修改前
BIOYOND_YB_Deck:
...
CoincellDeck:
...
# 修改后
BioyondElectrolyteDeck:
...
YihuaCoinCellDeck:
...
```
---
### 阶段 F — 物料余量监控集成原计划第5节细化
**目标**:弃用资源树内极片对象计数,改为直读依华扣电工站寄存器。
#### F1. `coin_cell_assembly/coin_cell_assembly.py` — 新增寄存器读取方法
参考 `coin_cell_assembly_b.csv` 中的地址,封装读取工具方法:
```python
MATERIAL_REGISTER_MAP = {
"10mm正极片": (520, "REAL"),
"12mm正极片": (522, "REAL"),
"16mm正极片": (524, "REAL"),
"铝箔": (526, "REAL"),
"正极壳": (528, "REAL"),
"平垫": (530, "REAL"),
"负极壳": (532, "REAL"),
"弹垫": (534, "REAL"),
"成品容量": (536, "REAL"),
"成品NG容量": (538, "REAL"),
}
def get_material_remaining(self, material_name: str) -> float:
"""通过寄存器直读指定物料的剩余数量"""
if material_name not in MATERIAL_REGISTER_MAP:
raise KeyError(f"未知物料名称: {material_name}")
address, dtype = MATERIAL_REGISTER_MAP[material_name]
return self.read_hold_register(address, dtype) # 复用现有 Modbus 读取方法
```
#### F2. 前端 data view 集成
- 前端点击 `MagazineHolder` 类资源的 data view 时,调用后端 `get_material_remaining` 接口(而非读取 `children` 长度)。
- 具体接口路径和前端调用代码需与前端开发同步,本文档不作具体实现约定。
---
## 4. 验证计划(细化)
### 4.1 单元测试(自动化)
```bash
# 序列化/反序列化往返测试
python -m pytest unilabos/test/ -k "serial" -v
# 特别检查以下错误消失:
# - ValueError: Resource '...' already assigned to deck
# - KeyError: 'liquid_history'
# - 重复 UUID 报错
```
### 4.2 集成测试(手动)
按以下顺序逐步验证,确保每步正常后再进行下一步:
1. **单独启动 `BatteryStation` 节点**,检查 `CoincellDeck`(现 `YihuaCoinCellDeck`)能否从数据库状态正确还原,无 `already assigned` 报错。
2. **单独启动 `BioyondElectrolyte` 节点**,检查 `BioyondElectrolyteDeck` 反序列化正常。
3. **同时启动两个节点**,模拟执行一次分液→扣电的完整跨站转运,确认:
- `electrolyte_buffer` 槽位正确接收分液瓶板。
- `bottle_rack_6x2` 初始为空,不出现虚拟瓶子。
4. **重启两个节点**(模拟断电恢复),确认资源树从数据库还原后,`electrolyte_buffer` 中仍持有正确的分液瓶板对象。
5. **寄存器余量读取**:手动触发 `get_material_remaining("负极壳")`,确认返回值与设备显示一致。
---
## 5. 与原计划的差异对照
| 维度 | 原计划 | 本文档新增/修订 |
|---|---|---|
| 执行顺序 | 未排序 | 明确 A→B→C→D→E→F 的依赖顺序 |
| `itemized_carrier.py` | 移除兜底补丁 | 补充:替换为带诊断信息的异常,便于排查 |
| `bottle_carriers.py` | 提及 `YIHUA_Electrolyte_12VialCarrier` 需修改 | 明确:删除第 54-55 行的瓶子填充循环 |
| `MaterialPlate` | 提及移除 `fill` 参数 | 说明保留 `fill=False` 路径;整体删除 `fill=True` 分支 |
| `deck.yaml` | 未提及 | **新增**:该注册文件也需要同步更新类名 |
| `resource_tracker.py` | 简略描述 | 提供具体的 `setdefault` 预处理代码示例 |
| 跨站转运 | 描述了问题和方向 | 细化:新增 `electrolyte_buffer` 槽位的具体名称和坐标;修正 `transfer` 目标查找方式 |
| 验证计划 | 简述目标 | 提供具体测试命令和逐步手动验证流程 |

View File

@@ -32,7 +32,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
MAX_SCAN_DEPTH = 10 # 最大目录递归深度
MAX_SCAN_FILES = 1000 # 最大扫描文件数量
_CACHE_VERSION = 1 # 缓存格式版本号,格式变更时递增
_CACHE_VERSION = 2 # 缓存格式版本号,格式变更时递增
# 合法的装饰器来源模块
_REGISTRY_DECORATOR_MODULE = "unilabos.registry.decorators"
@@ -258,8 +258,6 @@ def scan_directory(
}
# ---------------------------------------------------------------------------
# File-level parsing
# ---------------------------------------------------------------------------
@@ -361,6 +359,7 @@ def _parse_file(
"actions": class_body.get("actions", {}),
"status_properties": class_body.get("status_properties", {}),
"init_params": class_body.get("init_params", []),
"init_docstring": class_body.get("init_docstring"),
"auto_methods": class_body.get("auto_methods", {}),
"import_map": import_map,
}
@@ -497,7 +496,6 @@ def _collect_imports(tree: ast.Module, module_path: str = "") -> Dict[str, str]:
return import_map
# ---------------------------------------------------------------------------
# Decorator finding & argument extraction
# ---------------------------------------------------------------------------
@@ -768,6 +766,7 @@ def _extract_class_body(
"actions": {}, # method_name -> action_info
"status_properties": {}, # prop_name -> status_info
"init_params": [], # [{"name": ..., "type": ..., "default": ...}, ...]
"init_docstring": None,
"auto_methods": {}, # method_name -> method_info (no @action decorator)
}
@@ -780,6 +779,7 @@ def _extract_class_body(
# --- __init__ ---
if method_name == "__init__":
result["init_params"] = _extract_method_params(item, import_map)
result["init_docstring"] = ast.get_docstring(item)
continue
# --- Skip private/dunder ---

View File

@@ -51,14 +51,18 @@ Qone_nmr:
properties:
check_interval:
default: 60
description: 检查间隔时间默认60秒
type: string
expected_count:
default: 1
description: 期望生成的.nmr文件数量默认1个
type: string
monitor_dir:
description: 要监督的目录路径如果未指定则使用self.monitor_directory
type: string
stability_checks:
default: 3
description: 文件大小稳定性检查次数默认3次
type: string
required: []
type: object
@@ -85,11 +89,14 @@ Qone_nmr:
goal:
properties:
output_dir:
description: 输出目录如果未指定使用self.output_directory
type: string
string_list:
description: 字符串列表
type: string
txt_encoding:
default: utf-8
description: 文件编码
type: string
required:
- string_list
@@ -151,6 +158,13 @@ Qone_nmr:
additionalProperties: false
properties:
string:
description: '包含多个字符串的输入数据,支持两种格式:
1. 逗号分隔:如 "A 1 B 2 C 3, X 10 Y 20 Z 30"
2. 换行分隔:如 "A 1 B 2 C 3
X 10 Y 20 Z 30"'
type: string
title: StrSingleInput_Goal
type: object

File diff suppressed because it is too large Load Diff

View File

@@ -47,8 +47,10 @@ bioyond_dispensing_station:
goal:
properties:
report_request:
description: WorkstationReportRequest 对象,包含任务完成信息
type: string
used_materials:
description: 物料使用记录列表
type: string
required:
- report_request
@@ -102,6 +104,7 @@ bioyond_dispensing_station:
goal:
properties:
material_name:
description: 物料名称
type: string
required:
- material_name
@@ -611,10 +614,10 @@ bioyond_dispensing_station:
goal:
properties:
target_device_id:
description: 目标反应站设备ID(从设备列表中选择,所有转移组使用同一个目标设备
description: 目标反应站设备ID(所有转移组使用同一个设备)
type: string
transfer_groups:
description: 转移任务组列表每组包含物料名称、目标堆栈和目标库位,可以添加多组
description: '转移任务组列表,每组包含:'
type: array
required:
- target_device_id
@@ -694,10 +697,13 @@ bioyond_dispensing_station:
config:
properties:
config:
description: 配置字典,应包含material_type_mappings等配置
type: object
deck:
description: Deck对象
type: string
protocol_type:
description: 协议类型(由ROS系统传递,此处忽略)
type: string
required: []
type: object

View File

@@ -64,12 +64,59 @@ coincellassemblyworkstation_device:
properties: {}
required: []
type: object
result: {}
result:
type: boolean
required:
- goal
title: fun_wuliao_test参数
type: object
type: UniLabJsonCommand
auto-func_allpack_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: 4200
assembly_type: 7
elec_num: null
elec_use_num: null
elec_vol: 50
file_path: /Users/sml/work
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
default: 4200
type: integer
assembly_type:
default: 7
type: integer
elec_num:
type: string
elec_use_num:
type: string
elec_vol:
default: 50
type: integer
file_path:
default: /Users/sml/work
type: string
required:
- elec_num
- elec_use_num
type: object
result:
type: object
required:
- goal
title: func_allpack_cmd参数
type: object
type: UniLabJsonCommand
auto-func_allpack_cmd_simp:
feedback: {}
goal: {}
@@ -102,55 +149,54 @@ coincellassemblyworkstation_device:
goal:
properties:
assembly_pressure:
default: 3200
description: 电池压制力(N)
default: 4200
description: 电池压制力 (N)
type: integer
assembly_type:
default: 7
description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫)
description: 组装类型 (7=不用铝箔垫, 8=使用铝箔垫)
type: integer
battery_clean_ignore:
default: false
description: 是否忽略电池清洁步骤
description: 是否忽略电池清洁
type: boolean
battery_pressure_mode:
default: true
description: 是否启用压力模式
type: boolean
dual_drop_first_volume:
default: 0
description: 二次滴液第一次排液体积(μL)
default: 25
description: 二次滴液第一次排液体积 (μL)
type: integer
dual_drop_mode:
default: false
description: 电解液添加模式(false=单次滴液, true=二次滴液)
description: 电解液添加模式 (False=单次滴液, True=二次滴液)
type: boolean
dual_drop_start_timing:
default: false
description: 二次滴液开始滴液时机(false=正极片前, true=正极片后)
description: 二次滴液开始滴液时机 (False=正极片前, True=正极片后)
type: boolean
dual_drop_suction_timing:
default: false
description: 二次滴液吸液时机(false=正常吸液, true=先吸液)
description: 二次滴液吸液时机 (False=正常吸液, True=先吸液)
type: boolean
elec_num:
description: 电解液瓶数
type: string
elec_use_num:
default: 5
description: 每瓶电解液组装电池数
description: 每瓶电解液组装的电池数
type: string
elec_vol:
default: 50
description: 电解液吸液量(μL)
description: 电解液吸液量 (μL)
type: integer
file_path:
default: D:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly
default: /Users/sml/work
description: 实验记录保存路径
type: string
fujipian_juzhendianwei:
default: 0
description: 负极片矩阵点位。盘位置从1开始计数有效范围1-8, 13-20 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
description: 负极片矩阵点位
type: integer
fujipian_panshu:
default: 0
@@ -158,7 +204,7 @@ coincellassemblyworkstation_device:
type: integer
gemo_juzhendianwei:
default: 0
description: 隔膜矩阵点位。盘位置从1开始计数有效范围1-8, 13-20 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
description: 隔膜矩阵点位
type: integer
gemopanshu:
default: 0
@@ -170,13 +216,14 @@ coincellassemblyworkstation_device:
type: boolean
qiangtou_juzhendianwei:
default: 0
description: 枪头盒矩阵点位。盘位置从1开始计数有效范围1-32, 64-96 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
description: 枪头盒矩阵点位
type: integer
required:
- elec_num
- elec_use_num
type: object
result: {}
result:
type: object
required:
- goal
title: func_allpack_cmd_simp参数
@@ -261,11 +308,18 @@ coincellassemblyworkstation_device:
properties:
material_search_enable:
default: false
description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻)
description: '是否启用物料搜寻功能。
设备初始化后会弹出物料搜寻确认弹窗,
此参数控制自动点击''是''(启用)或''否''(不启用)。
默认为False不启用物料搜寻。'
type: boolean
required: []
type: object
result: {}
result:
type: boolean
required:
- goal
title: func_pack_device_init_auto_start_combined参数
@@ -307,7 +361,8 @@ coincellassemblyworkstation_device:
properties: {}
required: []
type: object
result: {}
result:
type: boolean
required:
- goal
title: func_pack_device_stop参数
@@ -332,7 +387,8 @@ coincellassemblyworkstation_device:
type: string
required: []
type: object
result: {}
result:
type: boolean
required:
- goal
title: func_pack_get_msg_cmd参数
@@ -346,10 +402,12 @@ coincellassemblyworkstation_device:
handles:
input:
- data_key: bottle_num
data_source: handle
data_source: workflow
data_type: integer
handler_key: bottle_count
io_type: source
label: 配液瓶数
required: true
placeholder_keys: {}
result: {}
schema:
@@ -384,7 +442,8 @@ coincellassemblyworkstation_device:
properties: {}
required: []
type: object
result: {}
result:
type: boolean
required:
- goal
title: func_pack_send_finished_cmd参数
@@ -421,7 +480,8 @@ coincellassemblyworkstation_device:
- assembly_type
- assembly_pressure
type: object
result: {}
result:
type: boolean
required:
- goal
title: func_pack_send_msg_cmd参数
@@ -477,21 +537,12 @@ coincellassemblyworkstation_device:
handles:
input:
- data_key: elec_num
data_source: handle
data_source: workflow
data_type: integer
handler_key: bottle_count
io_type: source
label: 配液瓶数
- data_key: formulations
data_source: handle
data_type: array
handler_key: formulations_input
label: 配方信息列表
output:
- data_key: assembly_data
data_source: executor
data_type: array
handler_key: assembly_data_output
label: 扣电组装数据列表
required: true
placeholder_keys: {}
result: {}
schema:
@@ -502,15 +553,15 @@ coincellassemblyworkstation_device:
properties:
assembly_pressure:
default: 4200
description: 电池压制力(N)
description: 电池压制力 (N)
type: integer
assembly_type:
default: 7
description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫)
description: 组装类型 (7=不用铝箔垫, 8=使用铝箔垫)
type: integer
battery_clean_ignore:
default: false
description: 是否忽略电池清洁步骤
description: 是否忽略电池清洁
type: boolean
battery_pressure_mode:
default: true
@@ -518,29 +569,29 @@ coincellassemblyworkstation_device:
type: boolean
dual_drop_first_volume:
default: 25
description: 二次滴液第一次排液体积(μL)
description: 二次滴液第一次排液体积 (μL)
type: integer
dual_drop_mode:
default: false
description: 电解液添加模式(false=单次滴液, true=二次滴液)
description: 电解液添加模式 (False=单次滴液, True=二次滴液)
type: boolean
dual_drop_start_timing:
default: false
description: 二次滴液开始滴液时机(false=正极片前, true=正极片后)
description: 二次滴液开始滴液时机 (False=正极片前, True=正极片后)
type: boolean
dual_drop_suction_timing:
default: false
description: 二次滴液吸液时机(false=正常吸液, true=先吸液)
description: 二次滴液吸液时机 (False=正常吸液, True=先吸液)
type: boolean
elec_num:
description: 电解液瓶数如果在workflow中已通过handles连接上游(create_orders的bottle_count输出),则此参数会自动从上游获取,无需手动填写;如果单独使用此函数(没有上游连接),则必须手动填写电解液瓶数
description: 电解液瓶数
type: string
elec_use_num:
description: 每瓶电解液组装电池数
description: 每瓶电解液组装电池数
type: string
elec_vol:
default: 50
description: 电解液吸液量(μL)
description: 电解液吸液量 (μL)
type: integer
file_path:
default: /Users/sml/work
@@ -548,7 +599,7 @@ coincellassemblyworkstation_device:
type: string
fujipian_juzhendianwei:
default: 0
description: 负极片矩阵点位。盘位置从1开始计数有效范围1-8, 13-20 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
description: 负极片矩阵点位
type: integer
fujipian_panshu:
default: 0
@@ -556,7 +607,7 @@ coincellassemblyworkstation_device:
type: integer
gemo_juzhendianwei:
default: 0
description: 隔膜矩阵点位。盘位置从1开始计数有效范围1-8, 13-20 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
description: 隔膜矩阵点位
type: integer
gemopanshu:
default: 0
@@ -568,13 +619,14 @@ coincellassemblyworkstation_device:
type: boolean
qiangtou_juzhendianwei:
default: 0
description: 枪头盒矩阵点位。盘位置从1开始计数有效范围1-32, 64-96 (写入值比实际位置少1例如写0取盘位1写1取盘位2)
description: 枪头盒矩阵点位
type: integer
required:
- elec_num
- elec_use_num
type: object
result: {}
result:
type: object
required:
- goal
title: func_sendbottle_allpack_multi参数
@@ -626,31 +678,6 @@ 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: {}
@@ -698,7 +725,8 @@ coincellassemblyworkstation_device:
required:
- fujipian_panshu
type: object
result: {}
result:
type: boolean
required:
- goal
title: qiming_coin_cell_code参数
@@ -706,10 +734,6 @@ coincellassemblyworkstation_device:
type: UniLabJsonCommand
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
status_types:
data_10mm_positive_plate_remaining: float
data_12mm_positive_plate_remaining: float
data_16mm_positive_plate_remaining: float
data_aluminum_foil_remaining: float
data_assembly_coin_cell_num: int
data_assembly_pressure: int
data_assembly_time: float
@@ -717,22 +741,14 @@ coincellassemblyworkstation_device:
data_axis_y_pos: float
data_axis_z_pos: float
data_coin_cell_code: str
data_coin_type: int
data_current_assembling_count: int
data_current_completed_count: int
data_coin_num: int
data_electrolyte_code: str
data_electrolyte_volume: int
data_finished_battery_ng_remaining_capacity: float
data_finished_battery_remaining_capacity: float
data_flat_washer_remaining: float
data_glove_box_o2_content: float
data_glove_box_pressure: float
data_glove_box_water_content: float
data_negative_shell_remaining: float
data_open_circuit_voltage: float
data_pole_weight: float
data_positive_shell_remaining: float
data_spring_washer_remaining: float
request_rec_msg_status: bool
request_send_msg_status: bool
sys_mode: str
@@ -762,14 +778,6 @@ coincellassemblyworkstation_device:
type: object
data:
properties:
data_10mm_positive_plate_remaining:
type: number
data_12mm_positive_plate_remaining:
type: number
data_16mm_positive_plate_remaining:
type: number
data_aluminum_foil_remaining:
type: number
data_assembly_coin_cell_num:
type: integer
data_assembly_pressure:
@@ -784,38 +792,22 @@ coincellassemblyworkstation_device:
type: number
data_coin_cell_code:
type: string
data_coin_type:
type: integer
data_current_assembling_count:
type: integer
data_current_completed_count:
data_coin_num:
type: integer
data_electrolyte_code:
type: string
data_electrolyte_volume:
type: integer
data_finished_battery_ng_remaining_capacity:
type: number
data_finished_battery_remaining_capacity:
type: number
data_flat_washer_remaining:
type: number
data_glove_box_o2_content:
type: number
data_glove_box_pressure:
type: number
data_glove_box_water_content:
type: number
data_negative_shell_remaining:
type: number
data_open_circuit_voltage:
type: number
data_pole_weight:
type: number
data_positive_shell_remaining:
type: number
data_spring_washer_remaining:
type: number
request_rec_msg_status:
type: boolean
request_send_msg_status:
@@ -825,36 +817,24 @@ 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_open_circuit_voltage
- data_assembly_pressure
- data_assembly_time
- data_axis_x_pos
- data_axis_y_pos
- data_axis_z_pos
- data_pole_weight
- data_assembly_pressure
- data_electrolyte_volume
- data_coin_type
- data_current_assembling_count
- data_current_completed_count
- data_coin_cell_code
- data_coin_num
- data_electrolyte_code
- data_glove_box_pressure
- data_electrolyte_volume
- data_glove_box_o2_content
- data_glove_box_pressure
- data_glove_box_water_content
- data_10mm_positive_plate_remaining
- data_12mm_positive_plate_remaining
- data_16mm_positive_plate_remaining
- data_aluminum_foil_remaining
- data_positive_shell_remaining
- data_flat_washer_remaining
- data_negative_shell_remaining
- data_spring_washer_remaining
- data_finished_battery_remaining_capacity
- data_finished_battery_ng_remaining_capacity
- data_open_circuit_voltage
- data_pole_weight
- request_rec_msg_status
- request_send_msg_status
- sys_mode
- sys_status
type: object
registry_type: device
version: 1.0.0

View File

@@ -18,6 +18,7 @@ xyz_stepper_controller:
goal:
properties:
degrees:
description: 角度值
type: number
required:
- degrees
@@ -44,6 +45,7 @@ xyz_stepper_controller:
goal:
properties:
axis:
description: 电机轴
type: object
required:
- axis
@@ -71,6 +73,7 @@ xyz_stepper_controller:
properties:
enable:
default: true
description: True为使能False为失能
type: boolean
required: []
type: object
@@ -99,9 +102,11 @@ xyz_stepper_controller:
goal:
properties:
axis:
description: 电机轴
type: object
enable:
default: true
description: True为使能False为失能
type: boolean
required:
- axis
@@ -152,6 +157,7 @@ xyz_stepper_controller:
goal:
properties:
axis:
description: 电机轴
type: object
required:
- axis
@@ -183,16 +189,21 @@ xyz_stepper_controller:
properties:
acceleration:
default: 1000
description: 加速度(rpm/s)
type: integer
axis:
description: 电机轴
type: object
position:
description: 目标位置(步数)
type: integer
precision:
default: 100
description: 到位精度
type: integer
speed:
default: 5000
description: 运行速度(rpm)
type: integer
required:
- axis
@@ -225,16 +236,21 @@ xyz_stepper_controller:
properties:
acceleration:
default: 1000
description: 加速度
type: integer
axis:
description: 电机轴
type: object
degrees:
description: 目标角度(度)
type: number
precision:
default: 100
description: 精度
type: integer
speed:
default: 5000
description: 移动速度
type: integer
required:
- axis
@@ -267,16 +283,21 @@ xyz_stepper_controller:
properties:
acceleration:
default: 1000
description: 加速度
type: integer
axis:
description: 电机轴
type: object
precision:
default: 100
description: 精度
type: integer
revolutions:
description: 目标圈数
type: number
speed:
default: 5000
description: 移动速度
type: integer
required:
- axis
@@ -309,15 +330,20 @@ xyz_stepper_controller:
properties:
acceleration:
default: 1000
description: 加速度
type: integer
speed:
default: 5000
description: 运行速度
type: integer
x:
description: X轴目标位置
type: integer
y:
description: Y轴目标位置
type: integer
z:
description: Z轴目标位置
type: integer
required: []
type: object
@@ -350,15 +376,20 @@ xyz_stepper_controller:
properties:
acceleration:
default: 1000
description: 加速度
type: integer
speed:
default: 5000
description: 移动速度
type: integer
x_deg:
description: X轴目标角度
type: number
y_deg:
description: Y轴目标角度
type: number
z_deg:
description: Z轴目标角度
type: number
required: []
type: object
@@ -391,15 +422,20 @@ xyz_stepper_controller:
properties:
acceleration:
default: 1000
description: 加速度
type: integer
speed:
default: 5000
description: 移动速度
type: integer
x_rev:
description: X轴目标圈数
type: number
y_rev:
description: Y轴目标圈数
type: number
z_rev:
description: Z轴目标圈数
type: number
required: []
type: object
@@ -427,6 +463,7 @@ xyz_stepper_controller:
goal:
properties:
revolutions:
description: 圈数
type: number
required:
- revolutions
@@ -456,10 +493,13 @@ xyz_stepper_controller:
properties:
acceleration:
default: 1000
description: 加速度(rpm/s)
type: integer
axis:
description: 电机轴
type: object
speed:
description: 运行速度(rpm),正值正转,负值反转
type: integer
required:
- axis
@@ -487,6 +527,7 @@ xyz_stepper_controller:
goal:
properties:
steps:
description: 步数
type: integer
required:
- steps
@@ -513,6 +554,7 @@ xyz_stepper_controller:
goal:
properties:
steps:
description: 步数
type: integer
required:
- steps
@@ -564,9 +606,11 @@ xyz_stepper_controller:
goal:
properties:
axis:
description: 电机轴
type: object
timeout:
default: 30.0
description: 超时时间(秒)
type: number
required:
- axis
@@ -591,11 +635,14 @@ xyz_stepper_controller:
properties:
baudrate:
default: 115200
description: 波特率
type: integer
port:
description: 串口端口名
type: string
timeout:
default: 1.0
description: 通信超时时间
type: number
required:
- port

View File

@@ -510,9 +510,11 @@ liquid_handler:
goal:
properties:
msg:
description: information to be printed
type: string
seconds:
default: 0
description: seconds to wait
type: string
required: []
type: object
@@ -2963,15 +2965,22 @@ liquid_handler:
additionalProperties: false
properties:
channel:
description: int
maximum: 2147483647
minimum: -2147483648
type: integer
dis_to_top:
description: 'float
Height in mm to move to relative to the well top.'
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number
well:
additionalProperties: false
description: 'Well
The target well.'
properties:
category:
type: string
@@ -4829,11 +4838,13 @@ liquid_handler:
config:
properties:
backend:
description: Backend to use.
type: object
channel_num:
default: 8
type: integer
deck:
description: Deck to use.
type: object
simulator:
default: false
@@ -4883,14 +4894,17 @@ liquid_handler.biomek:
bind_parent_id:
type: string
liquid_input_slot:
description: 液体输入槽列表
items:
type: integer
type: array
liquid_type:
description: 液体类型列表
items:
type: string
type: array
liquid_volume:
description: 液体体积列表
items:
type: integer
type: array
@@ -4901,6 +4915,7 @@ liquid_handler.biomek:
type: object
type: array
slot_on_deck:
description: 甲板上的槽位
type: integer
required:
- resource_tracker
@@ -5036,20 +5051,27 @@ liquid_handler.biomek:
additionalProperties: false
properties:
none_keys:
description: 需要设置为None的键列表
items:
type: string
type: array
protocol_author:
description: 协议作者
type: string
protocol_date:
description: 协议日期
type: string
protocol_description:
description: 协议描述
type: string
protocol_name:
description: 协议名称
type: string
protocol_type:
description: 协议类型
type: string
protocol_version:
description: 协议版本
type: string
title: LiquidHandlerProtocolCreation_Goal
type: object

File diff suppressed because it is too large Load Diff

View File

@@ -207,8 +207,12 @@ separator.homemade:
goal:
properties:
condition:
description: The condition to be monitored, either 'delta' or 'time'.
type: string
value:
description: 'The threshold value for the condition.
`delta > 0.05`, `time > 60`'
type: string
required:
- condition
@@ -305,12 +309,17 @@ separator.homemade:
event:
type: string
settling_time:
description: The duration for which to settle after stirring, in
seconds. Defaults to 10.
type: string
stir_speed:
description: The speed of stirring, in RPM. Defaults to 300.
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number
stir_time:
description: The duration for which to stir, in seconds. Defaults
to 10.
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number

View File

@@ -456,6 +456,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal:
properties:
volume:
description: 'absolute position of the plunger, unit: mL'
type: number
required:
- volume
@@ -481,6 +482,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal:
properties:
volume:
description: 'absolute position of the plunger, unit: mL'
type: number
required:
- volume
@@ -687,8 +689,10 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal:
properties:
max_velocity:
description: 'maximum velocity of the plunger, unit: ml/s'
type: number
position:
description: 'absolute position of the plunger, unit: ml'
type: number
required:
- position
@@ -1003,6 +1007,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal:
properties:
volume:
description: 'absolute position of the plunger, unit: mL'
type: number
required:
- volume
@@ -1028,6 +1033,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal:
properties:
volume:
description: 'absolute position of the plunger, unit: mL'
type: number
required:
- volume
@@ -1234,8 +1240,10 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal:
properties:
max_velocity:
description: 'maximum velocity of the plunger, unit: ml/s'
type: number
position:
description: 'absolute position of the plunger, unit: ml'
type: number
required:
- position

View File

@@ -32,7 +32,7 @@ reaction_station.bioyond:
type: integer
end_point:
default: 0
description: 终点计时点 (Start=开始前, End=结束后)
description: 终点计时点 (Start=0, End=1)
type: integer
end_step_key:
default: ''
@@ -40,11 +40,11 @@ reaction_station.bioyond:
type: string
start_point:
default: 0
description: 起点计时点 (Start=开始前, End=结束后)
description: 起点计时点 (Start=0, End=1)
type: integer
start_step_key:
default: ''
description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择)
description: 起点步骤Key (可选, 默认为空则自动选择)
type: string
required:
- duration
@@ -91,6 +91,7 @@ reaction_station.bioyond:
goal:
properties:
json_str:
description: 订单参数的JSON字符串
type: string
required:
- json_str
@@ -117,6 +118,7 @@ reaction_station.bioyond:
goal:
properties:
workflow_ids:
description: 要删除的工作流ID数组
items:
type: string
type: array
@@ -145,6 +147,7 @@ reaction_station.bioyond:
goal:
properties:
json_str:
description: 'JSON格式的字符串,包含:'
type: string
required:
- json_str
@@ -197,6 +200,7 @@ reaction_station.bioyond:
goal:
properties:
web_workflow_json:
description: JSON 格式的网页工作流列表
type: string
required:
- web_workflow_json
@@ -228,8 +232,10 @@ reaction_station.bioyond:
goal:
properties:
reactor_id:
description: 反应器编号 (1-5)
type: integer
temperature:
description: 目标温度 (°C)
type: number
required:
- reactor_id
@@ -257,6 +263,7 @@ reaction_station.bioyond:
goal:
properties:
preintake_id:
description: 通量ID
type: string
required:
- preintake_id
@@ -338,6 +345,7 @@ reaction_station.bioyond:
goal:
properties:
value:
description: 工作流 ID 列表
items:
type: string
type: array
@@ -365,6 +373,7 @@ reaction_station.bioyond:
goal:
properties:
workflow_id:
description: 工作流ID
type: string
required:
- workflow_id
@@ -424,11 +433,11 @@ reaction_station.bioyond:
goal:
properties:
assign_material_name:
description: 物料名称(不能为空)
description: 物料名称(液体种类)
type: string
temperature:
default: 25.0
description: 温度设定(°C)
description: 温度(C)
type: number
time:
default: '90'
@@ -436,14 +445,14 @@ reaction_station.bioyond:
type: string
titration_type:
default: '1'
description: 是否滴定(NO=, YES=)
description: 是否滴定(NO=1, YES=2)
type: string
torque_variation:
default: 2
description: 是否观察 (NO=, YES=)
description: 是否观察(NO=1, YES=2)
type: integer
volume:
description: 分液公式(mL)
description: 分液量(μL)
type: string
required:
- assign_material_name
@@ -525,11 +534,11 @@ reaction_station.bioyond:
properties:
assign_material_name:
default: BAPP
description: 物料名称
description: 物料名称(试剂瓶位)
type: string
temperature:
default: 25.0
description: 温度设定(°C)
description: 温度设定(C)
type: number
time:
default: '0'
@@ -537,15 +546,15 @@ reaction_station.bioyond:
type: string
titration_type:
default: '1'
description: 是否滴定(NO=, YES=)
description: 是否滴定(NO=1, YES=2)
type: string
torque_variation:
default: 1
description: 是否观察 (NO=否, YES=是)
description: 是否观察(int类型, 1=否, 2=是)
type: integer
volume:
default: '350'
description: 分液公式(mL)
description: 分液质量(g)
type: string
required: []
type: object
@@ -593,26 +602,28 @@ reaction_station.bioyond:
description: 物料名称
type: string
solvents:
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
description: '溶剂信息的字典或JSON字符串(可选),格式如下:
{'
type: string
temperature:
default: 25.0
description: 温度设定(°C),默认25.00
description: 温度设定(C)
type: number
time:
default: '360'
description: 观察时间(分钟),默认360
description: 观察时间(分钟)
type: string
titration_type:
default: '1'
description: 是否滴定(NO=, YES=是),默认NO
description: 是否滴定(NO=1, YES=2)
type: string
torque_variation:
default: 2
description: 是否观察 (NO=, YES=是),默认YES
description: 是否观察(NO=1, YES=2)
type: integer
volume:
description: 分液量(mL)。可直接提供,或通过solvents参数自动计算
description: 分液量(μL),直接指定体积(可选,如果提供solvents自动计算)
type: string
required:
- assign_material_name
@@ -671,33 +682,32 @@ reaction_station.bioyond:
description: 物料名称
type: string
extracted_actuals:
description: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh(m二酐滴定)和actualVolume(V二酐滴定)
description: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh和actualVolume
type: string
feeding_order_data:
description: 'feeding_order JSON对象,用于获取m二酐值(type为main_anhydride的amount)。示例:
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
description: feeding_order JSON字符串或对象,用于获取m二酐值
type: string
temperature:
default: 25.0
description: 温度设定(°C),默认25.00
description: 温度(C)
type: number
time:
default: '90'
description: 观察时间(分钟),默认90
description: 观察时间(分钟)
type: string
titration_type:
default: '2'
description: 是否滴定(NO=, YES=),默认YES
description: 是否滴定(NO=1, YES=2),默认2
type: string
torque_variation:
default: 2
description: 是否观察 (NO=, YES=是),默认YES
description: 是否观察(NO=1, YES=2)
type: integer
volume_formula:
description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
description: 分液公式(μL),如果提供则直接使用,否则自动计算
type: string
x_value:
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
description: 手工输入的x值,格式如 "1-2-3"
type: string
required:
- assign_material_name
@@ -738,7 +748,7 @@ reaction_station.bioyond:
type: string
temperature:
default: 25.0
description: 温度设定(°C)
description: 温度(C)
type: number
time:
default: '0'
@@ -746,14 +756,14 @@ reaction_station.bioyond:
type: string
titration_type:
default: '1'
description: 是否滴定(NO=, YES=)
description: 是否滴定(NO=1, YES=2)
type: string
torque_variation:
default: 1
description: 是否观察 (NO=, YES=)
description: 是否观察(NO=1, YES=2)
type: integer
volume_formula:
description: 分液公式(mL)
description: 分液公式(μL)
type: string
required:
- volume_formula
@@ -786,7 +796,7 @@ reaction_station.bioyond:
description: 任务名称
type: string
workflow_name:
description: 工作流名称
description: 合并后的工作流名称
type: string
required:
- workflow_name
@@ -819,15 +829,15 @@ reaction_station.bioyond:
goal:
properties:
assign_material_name:
description: 物料名称
description: 物料名称(不能为空)
type: string
cutoff:
default: '900000'
description: 粘度上限
description: 粘度上限(需为有效数字字符串,默认 "900000")
type: string
temperature:
default: -10.0
description: 温度设定(°C)
description: 温度设定(C,范围:-50.00 至 100.00)
type: number
required:
- assign_material_name
@@ -909,11 +919,11 @@ reaction_station.bioyond:
description: 物料名称(用于获取试剂瓶位ID)
type: string
material_id:
description: 粉末类型IDSalt=21分钟Flour=面粉27分钟BTDA=BTDA38分钟
description: 粉末类型ID, Salt=1, Flour=2, BTDA=3
type: string
temperature:
default: 25.0
description: 温度设定(°C)
description: 温度设定(C)
type: number
time:
default: '0'
@@ -921,7 +931,7 @@ reaction_station.bioyond:
type: string
torque_variation:
default: 1
description: 是否观察 (NO=, YES=)
description: 是否观察(NO=1, YES=2)
type: integer
required:
- material_id
@@ -945,10 +955,13 @@ reaction_station.bioyond:
config:
properties:
config:
description: 配置字典,应包含workflow_mappings等配置
type: object
deck:
description: Deck对象
type: string
protocol_type:
description: 协议类型(由ROS系统传递,此处忽略)
type: string
required: []
type: object

View File

@@ -198,6 +198,8 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
additionalProperties: false
properties:
command:
description: A JSON-formatted string that includes option, target,
speed, lift_height, mt_height
type: string
title: SendCmd_Goal
type: object
@@ -241,6 +243,8 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
additionalProperties: false
properties:
command:
description: A JSON-formatted string that includes quaternion, speed,
position
type: string
title: SendCmd_Goal
type: object
@@ -284,6 +288,7 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
additionalProperties: false
properties:
command:
description: A JSON-formatted string that includes speed
type: string
title: SendCmd_Goal
type: object

View File

@@ -709,6 +709,8 @@ linear_motion.toyo_xyz.sim:
additionalProperties: false
properties:
command:
description: A JSON-formatted string that includes option, target,
speed, lift_height, mt_height
type: string
title: SendCmd_Goal
type: object
@@ -752,6 +754,8 @@ linear_motion.toyo_xyz.sim:
additionalProperties: false
properties:
command:
description: A JSON-formatted string that includes quaternion, speed,
position
type: string
title: SendCmd_Goal
type: object
@@ -795,6 +799,7 @@ linear_motion.toyo_xyz.sim:
additionalProperties: false
properties:
command:
description: A JSON-formatted string that includes speed
type: string
title: SendCmd_Goal
type: object

View File

@@ -2179,6 +2179,7 @@ virtual_multiway_valve:
goal:
properties:
port_number:
description: 端口号 (1-8)
type: integer
required:
- port_number
@@ -2225,6 +2226,7 @@ virtual_multiway_valve:
goal:
properties:
port_number:
description: 目标端口号 (1-8)
type: integer
required:
- port_number
@@ -2261,6 +2263,7 @@ virtual_multiway_valve:
additionalProperties: false
properties:
command:
description: 目标位置 (0-8) 或位置字符串
type: string
title: SendCmd_Goal
type: object
@@ -2304,6 +2307,7 @@ virtual_multiway_valve:
additionalProperties: false
properties:
command:
description: 目标位置 (0-8) 或位置字符串
type: string
title: SendCmd_Goal
type: object
@@ -3960,6 +3964,14 @@ virtual_separator:
io_type: source
label: bottom_phase_out
side: SOUTH
- data_key: top_outlet
data_source: executor
data_type: fluid
description: 上相(轻相)液体输出口
handler_key: topphaseout
io_type: source
label: top_phase_out
side: NORTH
- data_key: mechanical_port
data_source: handle
data_type: mechanical
@@ -4207,6 +4219,7 @@ virtual_solenoid_valve:
additionalProperties: false
properties:
string:
description: '"ON"/"OFF" 或 "OPEN"/"CLOSED"'
type: string
title: StrSingleInput_Goal
type: object
@@ -4250,6 +4263,7 @@ virtual_solenoid_valve:
additionalProperties: false
properties:
command:
description: '"OPEN"/"CLOSED" 或其他控制命令'
type: string
title: SendCmd_Goal
type: object
@@ -4410,16 +4424,20 @@ virtual_solid_dispenser:
event:
type: string
mass:
description: 质量字符串 (如 "2.9 g")
type: string
mol:
description: 摩尔数字符串 (如 "0.12 mol")
type: string
purpose:
description: 添加目的
type: string
rate_spec:
type: string
ratio:
type: string
reagent:
description: 试剂名称
type: string
stir:
type: boolean
@@ -4431,6 +4449,7 @@ virtual_solid_dispenser:
type: string
vessel:
additionalProperties: false
description: 目标容器
properties:
category:
type: string
@@ -5560,8 +5579,10 @@ virtual_transfer_pump:
goal:
properties:
velocity:
description: 拉取速度 (ml/s)
type: number
volume:
description: 要拉取的体积 (ml)
type: number
required:
- volume
@@ -5588,8 +5609,10 @@ virtual_transfer_pump:
goal:
properties:
velocity:
description: 推出速度 (ml/s)
type: number
volume:
description: 要推出的体积 (ml)
type: number
required:
- volume
@@ -5685,10 +5708,12 @@ virtual_transfer_pump:
additionalProperties: false
properties:
max_velocity:
description: 移动速度 (ml/s)
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number
position:
description: 目标位置 (ml)
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number
@@ -5837,8 +5862,10 @@ virtual_transfer_pump:
config:
properties:
config:
description: 配置字典包含max_volume, port等参数
type: object
device_id:
description: 设备ID
type: string
required: []
type: object

View File

@@ -409,11 +409,11 @@ xrd_d7mate:
properties:
end_theta:
default: 80.0
description: 结束角度≥5.5°且必须大于start_theta
description: 结束角度≥5.5°,且必须大于 start_theta
type: number
exp_time:
default: 0.1
description: 曝光时间0.1-5.0秒)
description: 曝光时间0.1-5.0 秒)
type: number
increment:
default: 0.05
@@ -421,7 +421,7 @@ xrd_d7mate:
type: number
sample_id:
default: ''
description: 样品标识符
description: 样品名称
type: string
start_theta:
default: 10.0
@@ -433,7 +433,7 @@ xrd_d7mate:
type: string
wait_minutes:
default: 3.0
description: 允许上样后等待分钟数
description: 允许上样后、发送样品准备完成前的等待分钟数(默认 3 分钟)
type: number
required: []
title: StartWorkflow_Goal
@@ -492,12 +492,15 @@ xrd_d7mate:
properties:
host:
default: 127.0.0.1
description: 设备IP地址
type: string
port:
default: 6001
description: 通信端口默认6001
type: string
timeout:
default: 10.0
description: 超时时间,单位秒
type: string
required: []
type: object

View File

@@ -217,6 +217,7 @@ zhida_gcms:
additionalProperties: false
properties:
string:
description: Base64编码的CSV数据ROS2参数名
type: string
title: StrSingleInput_Goal
type: object
@@ -257,6 +258,7 @@ zhida_gcms:
additionalProperties: false
properties:
string:
description: CSV文件路径ROS2参数名
type: string
title: StrSingleInput_Goal
type: object
@@ -289,12 +291,15 @@ zhida_gcms:
properties:
host:
default: 192.168.3.184
description: 设备IP地址本地部署时可使用'127.0.0.1'
type: string
port:
default: 5792
description: 通信端口默认5792
type: string
timeout:
default: 10.0
description: 超时时间,单位秒
type: string
required: []
type: object

View File

@@ -271,6 +271,7 @@ class Registry:
registry_cache.pkl 一个文件中,删除即可完全重置。
"""
import time as _time
from unilabos.registry.ast_registry_scanner import _CACHE_VERSION as AST_SCAN_CACHE_VERSION
from unilabos.registry.ast_registry_scanner import scan_directory
scan_t0 = _time.perf_counter()
@@ -286,6 +287,10 @@ class Registry:
# ---- 统一缓存:一个 pkl 包含所有数据 ----
unified_cache = self._load_config_cache()
ast_cache = unified_cache.setdefault("_ast_scan", {"files": {}})
if ast_cache.get("version") != AST_SCAN_CACHE_VERSION:
ast_cache = {"version": AST_SCAN_CACHE_VERSION, "files": {}}
unified_cache["_ast_scan"] = ast_cache
unified_cache.pop("_build_results", None)
# 默认:扫描 unilabos 包所在的父目录
pkg_root = Path(__file__).resolve().parent.parent # .../unilabos
@@ -561,13 +566,47 @@ class Registry:
return prop_schema
@staticmethod
def _apply_docstring_param_metadata(
schema: Dict[str, Any],
doc_info: Dict[str, Any],
field_to_param: Optional[Dict[str, str]] = None,
apply_defaults: bool = False,
) -> None:
"""Apply parsed docstring display names and descriptions to schema properties."""
if not schema or not doc_info:
return
props = schema.get("properties", {})
if not isinstance(props, dict):
return
param_descs = doc_info.get("params", {}) or {}
param_display_names = doc_info.get("param_display_names", {}) or {}
for field_name, prop_schema in props.items():
if not isinstance(prop_schema, dict):
continue
param_name = field_to_param.get(field_name, field_name) if field_to_param else field_name
if not isinstance(param_name, str):
continue
param_name = param_name.removesuffix("[]")
if param_name in param_display_names:
prop_schema["title"] = param_display_names[param_name]
elif apply_defaults and not prop_schema.get("title"):
prop_schema["title"] = field_name
if param_name in param_descs:
prop_schema["description"] = param_descs[param_name]
elif apply_defaults and "description" not in prop_schema:
prop_schema["description"] = ""
def _generate_unilab_json_command_schema(
self, method_args: list, docstring: Optional[str] = None,
import_map: Optional[Dict[str, str]] = None,
apply_doc_defaults: bool = False,
) -> Dict[str, Any]:
"""根据方法参数和 docstring 生成 UniLabJsonCommand schema"""
doc_info = parse_docstring(docstring)
param_descs = doc_info.get("params", {})
schema = {
"type": "object",
@@ -598,12 +637,10 @@ class Registry:
param_name, param_type, param_default, import_map=import_map
)
if param_name in param_descs:
schema["properties"][param_name]["description"] = param_descs[param_name]
if param_required:
schema["required"].append(param_name)
self._apply_docstring_param_metadata(schema, doc_info, apply_defaults=apply_doc_defaults)
return schema
def _generate_status_types_schema(self, status_methods: Dict[str, Any]) -> Dict[str, Any]:
@@ -799,6 +836,7 @@ class Registry:
type_str = "UniLabJsonCommandAsync" if is_async else "UniLabJsonCommand"
params = method_info.get("params", [])
method_doc = method_info.get("docstring")
method_doc_info = parse_docstring(method_doc)
goal_schema = self._generate_schema_from_ast_params(params, method_name, method_doc, imap)
if action_args is not None:
@@ -828,7 +866,11 @@ class Registry:
# action handles: 从 @action(handles=[...]) 提取并转换为标准格式
raw_handles = (action_args or {}).get("handles")
handles = normalize_ast_action_handles(raw_handles) if isinstance(raw_handles, list) else (raw_handles or {})
handles = (
normalize_ast_action_handles(raw_handles)
if isinstance(raw_handles, list)
else (raw_handles or {})
)
# placeholder_keys: 先从参数类型自动检测,再用装饰器显式配置覆盖/补充
pk = detect_placeholder_keys(params)
@@ -847,7 +889,12 @@ class Registry:
"goal": goal,
"feedback": (action_args or {}).get("feedback") or {},
"result": (action_args or {}).get("result") or {},
"schema": wrap_action_schema(goal_schema, action_name, result_schema=result_schema),
"schema": wrap_action_schema(
goal_schema,
action_name,
description=(action_args or {}).get("description") or method_doc_info.get("description", ""),
result_schema=result_schema,
),
"goal_default": goal_default,
"handles": handles,
"placeholder_keys": pk,
@@ -886,7 +933,11 @@ class Registry:
action_name = f"auto-{action_name}"
raw_handles = action_args.get("handles")
handles = normalize_ast_action_handles(raw_handles) if isinstance(raw_handles, list) else (raw_handles or {})
handles = (
normalize_ast_action_handles(raw_handles)
if isinstance(raw_handles, list)
else (raw_handles or {})
)
method_params = method_info.get("params", [])
@@ -979,7 +1030,10 @@ class Registry:
"schema": schema,
"goal_default": goal_default,
"handles": handles,
"placeholder_keys": {**detect_placeholder_keys(method_params), **(action_args.get("placeholder_keys") or {})},
"placeholder_keys": {
**detect_placeholder_keys(method_params),
**(action_args.get("placeholder_keys") or {}),
},
}
if action_args.get("always_free") or method_info.get("always_free"):
action_entry["always_free"] = True
@@ -988,13 +1042,22 @@ class Registry:
nt = normalize_enum_value(action_args.get("node_type"), NodeType)
if nt:
action_entry["node_type"] = nt
goal_schema_for_docs = action_entry.get("schema", {}).get("properties", {}).get("goal", {})
self._apply_docstring_param_metadata(
goal_schema_for_docs,
parse_docstring(method_info.get("docstring")),
goal,
apply_defaults=True,
)
action_value_mappings[action_name] = action_entry
action_value_mappings = dict(sorted(action_value_mappings.items()))
# --- init_param_schema = { config: <init_params>, data: <status_types> } ---
init_params = ast_meta.get("init_params", [])
config_schema = self._generate_schema_from_ast_params(init_params, "__init__", import_map=imap)
config_schema = self._generate_schema_from_ast_params(
init_params, "__init__", ast_meta.get("init_docstring"), import_map=imap
)
data_schema = self._generate_status_schema_from_ast(
ast_meta.get("status_properties", {}), imap
)
@@ -1042,7 +1105,6 @@ class Registry:
) -> Dict[str, Any]:
"""Generate JSON Schema from AST-extracted parameter list."""
doc_info = parse_docstring(docstring)
param_descs = doc_info.get("params", {})
schema: Dict[str, Any] = {
"type": "object",
@@ -1072,12 +1134,10 @@ class Registry:
pname, ptype, pdefault, import_map
)
if pname in param_descs:
schema["properties"][pname]["description"] = param_descs[pname]
if prequired:
schema["required"].append(pname)
self._apply_docstring_param_metadata(schema, doc_info, apply_defaults=True)
return schema
def _generate_status_schema_from_ast(
@@ -1807,7 +1867,7 @@ class Registry:
else:
action_key = f"auto-{k}"
goal_schema = self._generate_unilab_json_command_schema(
v["args"], import_map=enhanced_import_map
v["args"], docstring=v.get("docstring"), import_map=enhanced_import_map
)
ret_type = v.get("return_type", "")
result_schema = None
@@ -1816,7 +1876,13 @@ class Registry:
"result", ret_type, None, import_map=enhanced_import_map
)
old_cfg = old_action_configs.get(action_key) or old_action_configs.get(f"auto-{k}", {})
new_schema = wrap_action_schema(goal_schema, action_key, result_schema=result_schema)
doc_info = parse_docstring(v.get("docstring"))
new_schema = wrap_action_schema(
goal_schema,
action_key,
description=doc_info.get("description", ""),
result_schema=result_schema,
)
old_schema = old_cfg.get("schema", {})
if old_schema:
preserve_field_descriptions(new_schema, old_schema)
@@ -1882,6 +1948,12 @@ class Registry:
merged_pk = dict(old_cfg.get("placeholder_keys", {}))
merged_pk.update(detect_placeholder_keys(v["args"]))
goal_schema_for_docs = (
entry_schema.get("properties", {}).get("goal", {})
if isinstance(entry_schema, dict)
else {}
)
self._apply_docstring_param_metadata(goal_schema_for_docs, doc_info, entry_goal)
entry = {
"type": entry_type,
@@ -1902,7 +1974,8 @@ class Registry:
device_config["init_param_schema"] = {}
init_schema = self._generate_unilab_json_command_schema(
enhanced_info["init_params"], "__init__",
enhanced_info["init_params"],
docstring=enhanced_info.get("init_docstring"),
import_map=enhanced_import_map,
)
device_config["init_param_schema"]["config"] = init_schema
@@ -1949,7 +2022,9 @@ class Registry:
action_str_type_mapping[action_type_str] = target_type
if target_type is not None:
try:
action_config["goal_default"] = ROS2MessageInstance(target_type.Goal()).get_python_dict()
action_config["goal_default"] = ROS2MessageInstance(
target_type.Goal()
).get_python_dict()
except Exception:
action_config["goal_default"] = {}
prev_schema = action_config.get("schema", {})
@@ -2141,6 +2216,7 @@ class Registry:
"unilabos_device_id": {
"type": "string",
"default": "",
"title": "设备ID",
"description": "UniLabOS设备ID用于指定执行动作的具体设备实例",
},
**schema["properties"]["goal"]["properties"],
@@ -2212,7 +2288,14 @@ class Registry:
lab_registry = Registry()
def build_registry(registry_paths=None, devices_dirs=None, upload_registry=False, check_mode=False, complete_registry=False, external_only=False):
def build_registry(
registry_paths=None,
devices_dirs=None,
upload_registry=False,
check_mode=False,
complete_registry=False,
external_only=False,
):
"""
构建或获取Registry单例实例
"""
@@ -2226,7 +2309,12 @@ def build_registry(registry_paths=None, devices_dirs=None, upload_registry=False
if path not in current_paths:
lab_registry.registry_paths.append(path)
lab_registry.setup(devices_dirs=devices_dirs, upload_registry=upload_registry, complete_registry=complete_registry, external_only=external_only)
lab_registry.setup(
devices_dirs=devices_dirs,
upload_registry=upload_registry,
complete_registry=complete_registry,
external_only=external_only,
)
# 将 AST 扫描的字符串类型替换为实际 ROS2 消息类(仅查找 ROS2 类型,不 import 设备模块)
lab_registry.resolve_all_types()

View File

@@ -1,12 +0,0 @@
YIHUA_Electrolyte_12VialCarrier:
category:
- battery_bottle_carriers
class:
module: unilabos.resources.battery.bottle_carriers:YIHUA_Electrolyte_12VialCarrier
type: pylabrobot
description: YIHUA 12-vial electrolyte carrier for coin cell assembly workstation
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,140 +1,84 @@
YB_Vial_20mL:
YB_20ml_fenyeping:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_Vial_20mL
module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping
type: pylabrobot
description: YB_Vial_20mL
description: YB_20ml_fenyeping
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
YB_Vial_5mL:
YB_5ml_fenyeping:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_Vial_5mL
module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping
type: pylabrobot
description: YB_Vial_5mL
description: YB_5ml_fenyeping
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
YB_DosingHead_L:
YB_jia_yang_tou_da:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_DosingHead_L
module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da
type: pylabrobot
description: YB_DosingHead_L
description: YB_jia_yang_tou_da
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
YB_PrepBottle_60mL:
YB_pei_ye_da_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_PrepBottle_60mL
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle
type: pylabrobot
description: YB_PrepBottle_60mL
description: YB_pei_ye_da_Bottle
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
YB_PrepBottle_15mL:
YB_pei_ye_xiao_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_PrepBottle_15mL
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
type: pylabrobot
description: YB_PrepBottle_15mL
description: YB_pei_ye_xiao_Bottle
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
YB_Tip_5000uL:
YB_qiang_tou:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_Tip_5000uL
module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou
type: pylabrobot
description: YB_Tip_5000uL
description: YB_qiang_tou
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
YB_Tip_1000uL:
category:
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_Tip_1000uL
type: pylabrobot
description: YB_Tip_1000uL
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_Tip_50uL:
category:
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_Tip_50uL
type: pylabrobot
description: YB_Tip_50uL
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_NormalLiq_250mL_Bottle:
YB_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_NormalLiq_250mL_Bottle
module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle
type: pylabrobot
description: YB_NormalLiq_250mL_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_NormalLiq_100mL_Bottle:
category:
- YB_bottle_carriers
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_NormalLiq_100mL_Bottle
type: pylabrobot
description: YB_NormalLiq_100mL_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_HighVis_250mL_Bottle:
category:
- YB_bottle_carriers
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_HighVis_250mL_Bottle
type: pylabrobot
description: YB_HighVis_250mL_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_HighVis_100mL_Bottle:
category:
- YB_bottle_carriers
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_HighVis_100mL_Bottle
type: pylabrobot
description: YB_HighVis_100mL_Bottle
description: YB_ye_Bottle
handles: []
icon: ''
init_param_schema: {}

View File

@@ -1,29 +1,42 @@
YB_Vial_20mL_Carrier:
YB_100ml_yeti:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Vial_20mL_Carrier
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti
type: pylabrobot
description: YB_Vial_20mL_Carrier
description: YB_100ml_yeti
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_Vial_5mL_Carrier:
YB_20ml_fenyepingban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Vial_5mL_Carrier
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban
type: pylabrobot
description: YB_Vial_5mL_Carrier
description: YB_20ml_fenyepingban
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
YB_5ml_fenyepingban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban
type: pylabrobot
description: YB_5ml_fenyepingban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6StockCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier
@@ -32,10 +45,10 @@ YB_6StockCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6VialCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier
@@ -44,137 +57,112 @@ YB_6VialCarrier:
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_DosingHead_L_Carrier:
YB_gao_nian_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_DosingHead_L_Carrier
module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle
type: pylabrobot
description: YB_DosingHead_L_Carrier
description: YB_gao_nian_ye_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_PrepBottle_60mL_Carrier:
YB_gaonianye:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_PrepBottle_60mL_Carrier
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye
type: pylabrobot
description: YB_PrepBottle_60mL_Carrier
description: YB_gaonianye
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_PrepBottle_15mL_Carrier:
YB_jia_yang_tou_da_Carrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_PrepBottle_15mL_Carrier
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier
type: pylabrobot
description: YB_PrepBottle_15mL_Carrier
description: YB_jia_yang_tou_da_Carrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_TipRack_Mixed:
YB_peiyepingdaban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_Mixed
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban
type: pylabrobot
description: YB_TipRack_Mixed
description: YB_peiyepingdaban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_TipRack_5000uL:
YB_peiyepingxiaoban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_5000uL
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban
type: pylabrobot
description: YB_TipRack_5000uL
description: YB_peiyepingxiaoban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_TipRack_50uL:
YB_qiang_tou_he:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_50uL
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he
type: pylabrobot
description: YB_TipRack_50uL
description: YB_qiang_tou_he
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_Adapter_60mL:
YB_shi_pei_qi_kuai:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Adapter_60mL
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai
type: pylabrobot
description: YB_Adapter_60mL
description: YB_shi_pei_qi_kuai
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_NormalLiq_250mL_Carrier:
YB_ye:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_NormalLiq_250mL_Carrier
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye
type: pylabrobot
description: YB_NormalLiq_250mL_Carrier
description: YB_ye_Bottle_Carrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_NormalLiq_100mL_Carrier:
YB_ye_100ml_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_NormalLiq_100mL_Carrier
module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle
type: pylabrobot
description: YB_NormalLiq_100mL_Carrier
description: YB_ye_100ml_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_HighVis_250mL_Carrier:
category:
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_HighVis_250mL_Carrier
type: pylabrobot
description: YB_HighVis_250mL_Carrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_HighVis_100mL_Carrier:
category:
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_HighVis_100mL_Carrier
type: pylabrobot
description: YB_HighVis_100mL_Carrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -20,22 +20,22 @@ BIOYOND_PolymerReactionStation_Deck:
icon: 反应站.webp
init_param_schema: {}
version: 1.0.0
BioyondElectrolyteDeck:
BIOYOND_YB_Deck:
category:
- deck
class:
module: unilabos.resources.bioyond.decks:bioyond_electrolyte_deck
module: unilabos.resources.bioyond.decks:YB_Deck
type: pylabrobot
description: BIOYOND ElectrolyteFormulationStation Deck
handles: []
icon: 配液站.webp
init_param_schema: {}
version: 1.0.0
YihuaCoinCellDeck:
CoincellDeck:
category:
- deck
class:
module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:yihua_coin_cell_deck
module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:YH_Deck
type: pylabrobot
description: YIHUA CoinCellAssembly Deck
handles: []

View File

@@ -36,16 +36,40 @@ class ROSMsgNotFound(Exception):
# ---------------------------------------------------------------------------
_SECTION_RE = re.compile(r"^(\w[\w\s]*):\s*$")
_PARAM_HEADER_RE = re.compile(
r"^\s*(?P<name>\w[\w]*)\s*(?:\[(?P<display_name>[^\]]+)\])?(?:\s*\([^)]*\))?\s*$"
)
def _parse_docstring_param_header(param_part: str) -> Tuple[str, Optional[str]]:
"""Parse ``name[display_name]`` or Google-style ``name (type)``."""
match = _PARAM_HEADER_RE.match(param_part.strip())
if not match:
return param_part.strip().split("(")[0].strip(), None
display_name = match.group("display_name")
if display_name is not None:
display_name = display_name.strip() or None
return match.group("name").strip(), display_name
def parse_docstring(docstring: Optional[str]) -> Dict[str, Any]:
"""
解析 Google-style docstring提取描述和参数说明。
解析 docstring提取描述和参数说明。
支持:
- Google-style ``Args:`` / ``Parameters:`` 小节
- 直接参数行 ``field: desc``
- 带显示名参数行 ``field[Display Name]: desc``
Returns:
{"description": "短描述", "params": {"param1": "参数1描述", ...}}
{
"description": "短描述",
"params": {"param1": "参数1描述", ...},
"param_display_names": {"param1": "显示名", ...},
}
"""
result: Dict[str, Any] = {"description": "", "params": {}}
result: Dict[str, Any] = {"description": "", "params": {}, "param_display_names": {}}
if not docstring:
return result
@@ -53,33 +77,53 @@ def parse_docstring(docstring: Optional[str]) -> Dict[str, Any]:
if not lines:
return result
result["description"] = lines[0].strip()
in_args = False
current_section: Optional[str] = None
current_param: Optional[str] = None
current_display_name: Optional[str] = None
current_desc_parts: list = []
for line in lines[1:]:
def flush_current_param() -> None:
nonlocal current_param, current_display_name, current_desc_parts
if current_param is None:
return
result["params"][current_param] = "\n".join(current_desc_parts).strip()
if current_display_name:
result["param_display_names"][current_param] = current_display_name
current_param = None
current_display_name = None
current_desc_parts = []
first_line = lines[0].strip()
start_index = 0
if not _SECTION_RE.match(first_line) and ":" not in first_line:
result["description"] = first_line
start_index = 1
for line in lines[start_index:]:
stripped = line.strip()
if not stripped:
if current_param is not None:
current_desc_parts.append("")
continue
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")
flush_current_param()
current_section = section_match.group(1).lower()
in_args = current_section in ("args", "arguments", "parameters", "params")
continue
if not in_args:
parse_as_param = in_args or current_section is None
if not parse_as_param:
continue
if ":" in stripped and not stripped.startswith(" "):
if current_param is not None:
result["params"][current_param] = "\n".join(current_desc_parts).strip()
if ":" in stripped:
flush_current_param()
param_part, _, desc_part = stripped.partition(":")
param_name = param_part.strip().split("(")[0].strip()
param_name, display_name = _parse_docstring_param_header(param_part)
current_param = param_name
current_display_name = display_name
current_desc_parts = [desc_part.strip()]
elif current_param is not None:
aline = line
@@ -89,8 +133,7 @@ def parse_docstring(docstring: Optional[str]) -> Dict[str, Any]:
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()
flush_current_param()
return result

View File

@@ -1,6 +1,9 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.YB_bottles import (
YB_pei_ye_xiao_Bottle,
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
@@ -48,5 +51,6 @@ def YIHUA_Electrolyte_12VialCarrier(name: str) -> BottleCarrier:
carrier.num_items_x = 2
carrier.num_items_y = 6
carrier.num_items_z = 1
# 载架初始化为空,瓶子由实际转运操作填入,避免反序列化时重复 assign
for i in range(12):
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}")
return carrier

View File

@@ -135,7 +135,6 @@ class BatteryState(TypedDict):
open_circuit_voltage: float
assembly_pressure: float
electrolyte_volume: float
pole_weight: float # 极片称重 (mg)
info: Optional[str] # 附加信息
@@ -180,7 +179,6 @@ class Battery(Container):
open_circuit_voltage=0.0,
assembly_pressure=0.0,
electrolyte_volume=0.0,
pole_weight=0.0,
info=None
)

View File

@@ -53,28 +53,13 @@ class Magazine(ResourceStack):
return self.get_size_z()
def serialize(self) -> dict:
data = super().serialize()
# 物料余量由寄存器接管,不再持久化极片子节点,
# 防止旧数据写回数据库后下次启动时再次引发重复 UUID。
data["children"] = []
data.update({
return {
**super().serialize(),
"size_x": self.size_x or 10.0,
"size_y": self.size_y or 10.0,
"size_z": self.size_z or 10.0,
"max_sheets": self.max_sheets,
})
return data
@classmethod
def deserialize(cls, data: dict, allow_marshal: bool = False):
"""反序列化时丢弃极片子节点ElectrodeSheet 等)。
物料余量已由寄存器接管,不再在资源树中追踪每个极片实体。
清空 children 可防止数据库中的旧极片记录被重新加载,避免重复 UUID 报错。
"""
data = dict(data)
data["children"] = []
return super().deserialize(data, allow_marshal=allow_marshal)
}
class MagazineHolder(ItemizedResource):
@@ -235,7 +220,7 @@ def MagazineHolder_6_Cathode(
size_y=size_y,
size_z=size_z,
locations=locations,
klasses=None,
klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan],
hole_diameter=hole_diameter,
hole_depth=hole_depth,
max_sheets_per_hole=max_sheets_per_hole,
@@ -273,7 +258,7 @@ def MagazineHolder_6_Anode(
size_y=size_y,
size_z=size_z,
locations=locations,
klasses=None,
klasses=[SpringWasher, NegativeCan, NegativeCan, SpringWasher, NegativeCan, NegativeCan],
hole_diameter=hole_diameter,
hole_depth=hole_depth,
max_sheets_per_hole=max_sheets_per_hole,
@@ -350,7 +335,7 @@ def MagazineHolder_4_Cathode(
size_y=size_y,
size_z=size_z,
locations=locations,
klasses=None,
klasses=[AluminumFoil, PositiveElectrode, PositiveElectrode, PositiveElectrode],
hole_diameter=hole_diameter,
hole_depth=hole_depth,
max_sheets_per_hole=max_sheets_per_hole,

View File

@@ -2,18 +2,15 @@ from pylabrobot.resources import create_homogeneous_resources, Coordinate, Resou
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.YB_bottles import (
YB_DosingHead_L,
YB_NormalLiq_250mL_Bottle,
YB_NormalLiq_100mL_Bottle,
YB_HighVis_250mL_Bottle,
YB_HighVis_100mL_Bottle,
YB_Vial_5mL,
YB_Vial_20mL,
YB_PrepBottle_15mL,
YB_PrepBottle_60mL,
YB_Tip_5000uL,
YB_Tip_1000uL,
YB_Tip_50uL,
YB_jia_yang_tou_da,
YB_ye_Bottle,
YB_ye_100ml_Bottle,
YB_gao_nian_ye_Bottle,
YB_5ml_fenyeping,
YB_20ml_fenyeping,
YB_pei_ye_xiao_Bottle,
YB_pei_ye_da_Bottle,
YB_qiang_tou,
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
@@ -209,7 +206,7 @@ def YB_6VialCarrier(name: str) -> BottleCarrier:
return carrier
# 1瓶载架 - 单个中央位置
def YB_NormalLiq_250mL_Carrier(name: str) -> BottleCarrier:
def YB_ye(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -236,17 +233,17 @@ def YB_NormalLiq_250mL_Carrier(name: str) -> BottleCarrier:
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_NormalLiq_250mL_Carrier",
model="YB_ye",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_NormalLiq_250mL_Bottle(f"{name}_flask_1")
carrier[0] = YB_ye_Bottle(f"{name}_flask_1")
return carrier
# 高粘液瓶载架 - 单个中央位置
def YB_HighVis_250mL_Carrier(name: str) -> BottleCarrier:
def YB_gaonianye(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -273,17 +270,17 @@ def YB_HighVis_250mL_Carrier(name: str) -> BottleCarrier:
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_HighVis_250mL_Carrier",
model="YB_gaonianye",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_HighVis_250mL_Bottle(f"{name}_flask_1")
carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1")
return carrier
# 100mL普通液瓶载架 - 单个中央位置
def YB_NormalLiq_100mL_Carrier(name: str) -> BottleCarrier:
# 100ml液体瓶载架 - 单个中央位置
def YB_100ml_yeti(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -310,52 +307,16 @@ def YB_NormalLiq_100mL_Carrier(name: str) -> BottleCarrier:
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_NormalLiq_100mL_Carrier",
model="YB_100ml_yeti",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_NormalLiq_100mL_Bottle(f"{name}_flask_1")
carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1")
return carrier
# 100mL高粘液瓶载架 - 单个中央位置
def YB_HighVis_100mL_Carrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_HighVis_100mL_Carrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_HighVis_100mL_Bottle(f"{name}_flask_1")
return carrier
# 5mL分液瓶板 - 4x2布局8个位置
def YB_Vial_5mL_Carrier(name: str) -> BottleCarrier:
# 5ml分液瓶板 - 4x2布局8个位置
def YB_5ml_fenyepingban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
@@ -394,18 +355,18 @@ def YB_Vial_5mL_Carrier(name: str) -> BottleCarrier:
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_Vial_5mL_Carrier",
model="YB_5ml_fenyepingban",
)
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_Vial_5mL(f"{name}_vial_{ordering[i]}")
carrier[i] = YB_5ml_fenyeping(f"{name}_vial_{ordering[i]}")
return carrier
# 20mL分液瓶板 - 4x2布局8个位置
def YB_Vial_20mL_Carrier(name: str) -> BottleCarrier:
# 20ml分液瓶板 - 4x2布局8个位置
def YB_20ml_fenyepingban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
@@ -444,18 +405,18 @@ def YB_Vial_20mL_Carrier(name: str) -> BottleCarrier:
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_Vial_20mL_Carrier",
model="YB_20ml_fenyepingban",
)
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_Vial_20mL(f"{name}_vial_{ordering[i]}")
carrier[i] = YB_20ml_fenyeping(f"{name}_vial_{ordering[i]}")
return carrier
# 配液瓶(小)板 - 4x2布局8个位置
def YB_PrepBottle_15mL_Carrier(name: str) -> BottleCarrier:
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
@@ -494,19 +455,19 @@ def YB_PrepBottle_15mL_Carrier(name: str) -> BottleCarrier:
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_PrepBottle_15mL_Carrier",
model="YB_peiyepingxiaoban",
)
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_PrepBottle_15mL(f"{name}_bottle_{ordering[i]}")
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
# 配液瓶(大)板 - 2x2布局4个位置
def YB_PrepBottle_60mL_Carrier(name: str) -> BottleCarrier:
def YB_peiyepingdaban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -544,18 +505,18 @@ def YB_PrepBottle_60mL_Carrier(name: str) -> BottleCarrier:
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_PrepBottle_60mL_Carrier",
model="YB_peiyepingdaban",
)
carrier.num_items_x = 2
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "B1", "B2"]
for i in range(4):
carrier[i] = YB_PrepBottle_60mL(f"{name}_bottle_{ordering[i]}")
carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
# 加样头(大)板 - 1x1布局1个位置
def YB_DosingHead_L_Carrier(name: str) -> BottleCarrier:
def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -593,16 +554,16 @@ def YB_DosingHead_L_Carrier(name: str) -> BottleCarrier:
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_DosingHead_L_Carrier",
model="YB_jia_yang_tou_da_Carrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_DosingHead_L(f"{name}_head_1")
carrier[0] = YB_jia_yang_tou_da(f"{name}_head_1")
return carrier
def YB_Adapter_60mL(name: str) -> BottleCarrier:
def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier:
"""适配器块 - 单个中央位置"""
# 载架尺寸 (mm)
@@ -630,7 +591,7 @@ def YB_Adapter_60mL(name: str) -> BottleCarrier:
resource_size_y=adapter_diameter,
name_prefix=name,
),
model="YB_Adapter_60mL",
model="YB_shi_pei_qi_kuai",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
@@ -639,7 +600,7 @@ def YB_Adapter_60mL(name: str) -> BottleCarrier:
return carrier
def YB_TipRack_50uL(name: str) -> BottleCarrier:
def YB_qiang_tou_he(name: str) -> BottleCarrier:
"""枪头盒 - 8x12布局96个位置"""
# 载架尺寸 (mm)
@@ -648,9 +609,9 @@ def YB_TipRack_50uL(name: str) -> BottleCarrier:
carrier_size_z = 55.0
# 枪头尺寸
tip_diameter = 7.0
tip_spacing_x = 7.5 # X方向间距
tip_spacing_y = 7.5 # Y方向间距
tip_diameter = 10.0
tip_spacing_x = 9.0 # X方向间距
tip_spacing_y = 9.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2
@@ -678,7 +639,7 @@ def YB_TipRack_50uL(name: str) -> BottleCarrier:
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_TipRack_50uL",
model="YB_qiang_tou_he",
)
carrier.num_items_x = 12
carrier.num_items_y = 8
@@ -687,182 +648,6 @@ def YB_TipRack_50uL(name: str) -> BottleCarrier:
for i in range(96):
row = chr(65 + i // 12) # A-H
col = (i % 12) + 1 # 1-12
carrier[i] = YB_Tip_50uL(f"{name}_tip_{row}{col}")
return carrier
def YB_TipRack_5000uL(name: str) -> BottleCarrier:
"""枪头盒 - 4x6布局24个位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 95.0
# 枪头尺寸
tip_diameter = 16.0
tip_spacing_x = 16.5 # X方向间距
tip_spacing_y = 16.5 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (6 - 1) * tip_spacing_x - tip_diameter) / 2
start_y = (carrier_size_y - (4 - 1) * tip_spacing_y - tip_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=6,
num_items_y=4,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=tip_spacing_x,
item_dy=tip_spacing_y,
size_x=tip_diameter,
size_y=tip_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_TipRack_5000uL",
)
carrier.num_items_x = 6
carrier.num_items_y = 4
carrier.num_items_z = 1
# 创建24个枪头
for i in range(24):
row = chr(65 + i // 6) # A-D
col = (i % 6) + 1 # 1-6
carrier[i] = YB_Tip_5000uL(f"{name}_tip_{row}{col}")
return carrier
def YB_TipRack_Mixed(name: str) -> BottleCarrier:
"""混合枪头盒 - 复杂布局
上层: 2x8空位原50uL枪头位置现空余
中层: 4x4布局放5000uL枪头
下层: 2x8布局放1000uL枪头
"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 95.0
# 各类枪头的尺寸参数
tip_5000_diameter = 16.0
tip_5000_spacing_x = 16.5
tip_5000_spacing_y = 16.5
tip_1000_diameter = 7.0
tip_1000_spacing_x = 7.5
tip_1000_spacing_y = 7.5
# 空位尺寸上层2x8原50uL位置
empty_diameter = 7.0
empty_spacing_x = 7.5
empty_spacing_y = 7.5
# 计算各层的起始位置
# 上层空位 (2x8)
empty_top_start_x = (carrier_size_x - (8 - 1) * empty_spacing_x - empty_diameter) / 2
empty_top_start_y = 5.0
# 中层5000uL (4x4)
tip_5000_start_x = (carrier_size_x - (4 - 1) * tip_5000_spacing_x - tip_5000_diameter) / 2
tip_5000_start_y = empty_top_start_y + 2 * empty_spacing_y + 5.0
# 下层1000uL (2x8)
tip_1000_start_x = (carrier_size_x - (8 - 1) * tip_1000_spacing_x - tip_1000_diameter) / 2
tip_1000_start_y = tip_5000_start_y + 4 * tip_5000_spacing_y + 5.0
sites = {}
# 创建上层空位 (2x8) - 不创建实际的枪头对象
empty_top_sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=8,
num_items_y=2,
dx=empty_top_start_x,
dy=empty_top_start_y,
dz=5.0,
item_dx=empty_spacing_x,
item_dy=empty_spacing_y,
size_x=empty_diameter,
size_y=empty_diameter,
size_z=carrier_size_z,
)
# 添加空位,索引 0-15
for k, v in empty_top_sites.items():
v.name = f"{name}_empty_top_{v.name}"
sites[k] = v
# 创建中层5000uL枪头位 (4x4),索引 16-31
tip_5000_sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4,
num_items_y=4,
dx=tip_5000_start_x,
dy=tip_5000_start_y,
dz=15.0,
item_dx=tip_5000_spacing_x,
item_dy=tip_5000_spacing_y,
size_x=tip_5000_diameter,
size_y=tip_5000_diameter,
size_z=carrier_size_z,
)
for i, (k, v) in enumerate(tip_5000_sites.items()):
v.name = f"{name}_5000_{v.name}"
sites[16 + i] = v
# 创建下层1000uL枪头位 (2x8),索引 32-47
tip_1000_sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=8,
num_items_y=2,
dx=tip_1000_start_x,
dy=tip_1000_start_y,
dz=25.0,
item_dx=tip_1000_spacing_x,
item_dy=tip_1000_spacing_y,
size_x=tip_1000_diameter,
size_y=tip_1000_diameter,
size_z=carrier_size_z,
)
for i, (k, v) in enumerate(tip_1000_sites.items()):
v.name = f"{name}_1000_{v.name}"
sites[32 + i] = v
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_TipRack_Mixed",
)
carrier.num_items_x = 8 # 最大宽度
carrier.num_items_y = 8 # 总行数 (2+4+2)
carrier.num_items_z = 1
# 为5000uL枪头创建实例 (16个),对应索引 16-31
for i in range(16):
row = chr(65 + i // 4) # A-D
col = (i % 4) + 1 # 1-4
carrier[16 + i] = YB_Tip_5000uL(f"{name}_tip5000_{row}{col}")
# 为1000uL枪头创建实例 (16个),对应索引 32-47
for i in range(16):
row = chr(65 + i // 8) # A-B
col = (i % 8) + 1 # 1-8
carrier[32 + i] = YB_Tip_1000uL(f"{name}_tip1000_{row}{col}")
carrier[i] = YB_qiang_tou(f"{name}_tip_{row}{col}")
return carrier

View File

@@ -1,7 +1,7 @@
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数
"""加样头(大)"""
def YB_DosingHead_L(
def YB_jia_yang_tou_da(
name: str,
diameter: float = 20.0,
height: float = 100.0,
@@ -15,11 +15,11 @@ def YB_DosingHead_L(
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_DosingHead_L",
model="YB_jia_yang_tou_da",
)
"""250mL普通"""
def YB_NormalLiq_250mL_Bottle(
"""1x1"""
def YB_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
@@ -33,105 +33,87 @@ def YB_NormalLiq_250mL_Bottle(
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_NormalLiq_250mL_Bottle",
model="YB_ye_Bottle",
)
"""100mL普通液"""
def YB_NormalLiq_100mL_Bottle(
"""100ml液体"""
def YB_ye_100ml_Bottle(
name: str,
diameter: float = 50.0,
height: float = 90.0,
max_volume: float = 100000.0, # 100mL
barcode: str = None,
) -> Bottle:
"""创建100mL普通液"""
"""创建100ml液体"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_NormalLiq_100mL_Bottle",
model="YB_100ml_yeti",
)
"""100mL高粘液"""
def YB_HighVis_100mL_Bottle(
name: str,
diameter: float = 50.0,
height: float = 90.0,
max_volume: float = 100000.0, # 100mL
barcode: str = None,
) -> Bottle:
"""创建100mL高粘液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_HighVis_100mL_Bottle",
)
"""250mL高粘液"""
def YB_HighVis_250mL_Bottle(
"""高粘液"""
def YB_gao_nian_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
max_volume: float = 50000.0, # 50mL
barcode: str = None,
) -> Bottle:
"""创建250mL高粘液瓶"""
"""创建高粘液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_HighVis_250mL_Bottle",
model="High_Viscosity_Liquid",
)
"""5mL分液瓶"""
def YB_Vial_5mL(
"""5ml分液瓶"""
def YB_5ml_fenyeping(
name: str,
diameter: float = 20.0,
height: float = 50.0,
max_volume: float = 5000.0, # 5mL
barcode: str = None,
) -> Bottle:
"""创建5mL分液瓶"""
"""创建5ml分液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_Vial_5mL",
model="YB_5ml_fenyeping",
)
"""20mL分液瓶"""
def YB_Vial_20mL(
"""20ml分液瓶"""
def YB_20ml_fenyeping(
name: str,
diameter: float = 30.0,
height: float = 65.0,
max_volume: float = 20000.0, # 20mL
barcode: str = None,
) -> Bottle:
"""创建20mL分液瓶"""
"""创建20ml分液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_Vial_20mL",
model="YB_20ml_fenyeping",
)
"""配液瓶(小)"""
def YB_PrepBottle_15mL(
def YB_pei_ye_xiao_Bottle(
name: str,
diameter: float = 35.0,
height: float = 60.0,
max_volume: float = 15000.0, # 15mL
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建配液瓶(小)"""
@@ -141,15 +123,15 @@ def YB_PrepBottle_15mL(
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_PrepBottle_15mL",
model="YB_pei_ye_xiao_Bottle",
)
"""配液瓶(大)"""
def YB_PrepBottle_60mL(
def YB_pei_ye_da_Bottle(
name: str,
diameter: float = 55.0,
height: float = 100.0,
max_volume: float = 60000.0, # 60mL
max_volume: float = 150000.0, # 150mL
barcode: str = None,
) -> Bottle:
"""创建配液瓶(大)"""
@@ -159,29 +141,11 @@ def YB_PrepBottle_60mL(
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_PrepBottle_60mL",
model="YB_pei_ye_da_Bottle",
)
"""5000uL枪头"""
def YB_Tip_5000uL(
name: str,
diameter: float = 10.0,
height: float = 50.0,
max_volume: float = 5000.0, # 5mL
barcode: str = None,
) -> Bottle:
"""创建枪头"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_Tip_5000uL",
)
"""1000uL枪头"""
def YB_Tip_1000uL(
"""枪头"""
def YB_qiang_tou(
name: str,
diameter: float = 10.0,
height: float = 50.0,
@@ -195,23 +159,5 @@ def YB_Tip_1000uL(
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_Tip_1000uL",
model="YB_qiang_tou",
)
"""50uL枪头"""
def YB_Tip_50uL(
name: str,
diameter: float = 10.0,
height: float = 50.0,
max_volume: float = 50.0, # 50uL
barcode: str = None,
) -> Bottle:
"""创建枪头"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_Tip_50uL",
)

View File

@@ -1,3 +1,4 @@
from os import name
from pylabrobot.resources import Deck, Coordinate, Rotation
from unilabos.resources.bioyond.YB_warehouses import (
@@ -33,8 +34,11 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
size_y: float = 1080.0,
size_z: float = 1500.0,
category: str = "deck",
setup: bool = False
) -> None:
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
if setup:
self.setup()
def setup(self) -> None:
# 添加仓库
@@ -62,7 +66,6 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
class BIOYOND_PolymerPreparationStation_Deck(Deck):
def __init__(
self,
@@ -71,8 +74,11 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
size_y: float = 1080.0,
size_z: float = 1500.0,
category: str = "deck",
setup: bool = False
) -> None:
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
if setup:
self.setup()
def setup(self) -> None:
# 添加仓库 - 配液站的3个堆栈使用Bioyond系统中的实际名称
@@ -95,8 +101,7 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
class BioyondElectrolyteDeck(Deck):
class BIOYOND_YB_Deck(Deck):
def __init__(
self,
name: str = "YB_Deck",
@@ -104,7 +109,7 @@ class BioyondElectrolyteDeck(Deck):
size_y: float = 1400.0,
size_z: float = 2670.0,
category: str = "deck",
setup: bool = False,
setup: bool = False
) -> None:
super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0)
if setup:
@@ -113,8 +118,8 @@ class BioyondElectrolyteDeck(Deck):
def setup(self) -> None:
# 添加仓库
self.warehouses = {
"自动堆栈-左": bioyond_warehouse_2x2x1("自动堆栈-左"), # 2行×2列
"自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"), # 2行×2列
"321窗口": bioyond_warehouse_2x2x1("321窗口"), # 2行×2列
"43窗口": bioyond_warehouse_2x2x1("43窗口"), # 2行×2列
"手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03
"手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03
"加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"),
@@ -128,34 +133,29 @@ class BioyondElectrolyteDeck(Deck):
}
# warehouse 的位置
self.warehouse_locations = {
"自动堆栈-左": Coordinate(-150.0, 1142.0, 0.0),
"自动堆栈-右": Coordinate(4160.0, 1142.0, 0.0),
"手动传递窗左": Coordinate(-150.0, 423.0, 0.0),
"手动传递窗右": Coordinate(4160.0, 423.0, 0.0),
"加样头堆栈左": Coordinate(385.0, 0, 0.0),
"加样头堆栈右": Coordinate(2187.0, 0, 0.0),
"321窗口": Coordinate(-150.0, 158.0, 0.0),
"43窗口": Coordinate(4160.0, 158.0, 0.0),
"手动传递窗左": Coordinate(-150.0, 877.0, 0.0),
"手动传递窗右": Coordinate(4160.0, 877.0, 0.0),
"加样头堆栈左": Coordinate(385.0, 1300.0, 0.0),
"加样头堆栈右": Coordinate(2187.0, 1300.0, 0.0),
"15ml配液堆栈左": Coordinate(749.0, 945.0, 0.0),
"母液加样右": Coordinate(2152.0, 967.0, 0.0),
"大瓶母液堆栈左": Coordinate(1164.0, 624.0, 0.0),
"大瓶母液堆栈右": Coordinate(2717.0, 624.0, 0.0),
"2号手套箱内部堆栈": Coordinate(-800, 800.0, 0.0), # 新增:位置需根据实际硬件调整
"15ml配液堆栈左": Coordinate(749.0, 355.0, 0.0),
"母液加样右": Coordinate(2152.0, 333.0, 0.0),
"大瓶母液堆栈左": Coordinate(1164.0, 676.0, 0.0),
"大瓶母液堆栈右": Coordinate(2717.0, 676.0, 0.0),
"2号手套箱内部堆栈": Coordinate(-800, -500.0, 0.0), # 新增:位置需根据实际硬件调整
}
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
# 向后兼容别名,日后废弃
BIOYOND_YB_Deck = BioyondElectrolyteDeck
def YB_Deck(name: str) -> Deck:
by=BIOYOND_YB_Deck(name=name)
by.setup()
return by
def bioyond_electrolyte_deck(name: str) -> BioyondElectrolyteDeck:
deck = BioyondElectrolyteDeck(name=name)
deck.setup()
return deck
# 向后兼容别名,日后废弃
def YB_Deck(name: str) -> BioyondElectrolyteDeck:
return bioyond_electrolyte_deck(name)

View File

@@ -797,10 +797,9 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
bottle = plr_material[number] = initialize_resource(
{"name": f'{detail["name"]}_{number}', "class": reverse_type_mapping[typeName][0]}, resource_type=ResourcePLR
)
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
bottle.tracker.liquids = [
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
]
bottle.tracker.liquids = [
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
]
bottle.code = detail.get("code", "")
logger.debug(f" └─ [子物料] {detail['name']}{plr_material.name}[{number}] (类型:{typeName})")
else:
@@ -809,10 +808,9 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
# 只对有 capacity 属性的容器(液体容器)处理液体追踪
if hasattr(plr_material, 'capacity'):
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
bottle.tracker.liquids = [
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
]
bottle.tracker.liquids = [
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
]
plr_materials.append(plr_material)

View File

@@ -179,35 +179,6 @@ class ItemizedCarrier(ResourcePLR):
idx = i
break
if idx is None and location is not None:
# 精确坐标匹配失败常见原因DB 存储的 z=0而槽位定义 z=dz>0
# 降级为仅按 XY 坐标进行近似匹配,找到后使用槽位自身的正确坐标写回,
# 避免因 Z 偏移导致反序列化中断。
_XY_TOLERANCE = 2.0 # mm覆盖浮点误差和 z 偏移
min_dist = float("inf")
nearest_idx = None
for _i, _loc in enumerate(self.child_locations.values()):
_d = (((_loc.x - location.x) ** 2) + ((_loc.y - location.y) ** 2)) ** 0.5
if _d < min_dist:
min_dist = _d
nearest_idx = _i
if nearest_idx is not None and min_dist <= _XY_TOLERANCE:
from unilabos.utils.log import logger as _logger
_slot_label = list(self.child_locations.keys())[nearest_idx]
_logger.warning(
f"[ItemizedCarrier '{self.name}'] 资源 '{resource.name}' 坐标 {location} 与槽位 "
f"'{_slot_label}' {list(self.child_locations.values())[nearest_idx]} 的 XY 吻合"
f"XY 偏差={min_dist:.2f}mm按 XY 近似匹配成功z 偏移已被修正。"
)
idx = nearest_idx
if idx is None:
raise ValueError(
f"[ItemizedCarrier '{self.name}'] 无法为资源 '{resource.name}' 找到匹配的槽位。\n"
f" 已知槽位: {list(self.child_locations.keys())}\n"
f" 传入坐标: {location}\n"
f" 提示: XY 近似匹配也失败,请检查资源坐标或 Carrier 槽位定义是否正确。"
)
if not reassign and self.sites[idx] is not None:
raise ValueError(f"a site with index {idx} already exists")
location = list(self.child_locations.values())[idx]

View File

@@ -612,31 +612,6 @@ class ResourceTreeSet(object):
d["model"] = res.config.get("model", None)
return d
def _deduplicate_plr_dict(d: dict, _seen: set = None) -> dict:
"""递归去除 children 中同名重复节点(全树范围、保留首次出现)。
根本原因:同一槽位被 sync_from_externalBioyond 同步)重复写入,
导致数据库中同一 WareHouse 下存在多条同名 BottleCarrier 记录(不同 UUID
PLR 的 _check_naming_conflicts 在全树范围检查名称唯一性,
重复名称会在 deserialize 时抛出 ValueError导致节点启动失败。
此函数在 sub_cls.deserialize 前预先清理,保证名称唯一。
"""
if _seen is None:
_seen = set()
children = d.get("children", [])
deduped = []
for child in children:
child = _deduplicate_plr_dict(child, _seen)
cname = child.get("name")
if cname not in _seen:
_seen.add(cname)
deduped.append(child)
else:
logger.warning(
f"[资源树去重] 发现重复资源名称 '{cname}',跳过重复项(历史脏数据)"
)
return {**d, "children": deduped}
plr_resources = []
tracker = DeviceNodeResourceTracker()
@@ -647,8 +622,6 @@ class ResourceTreeSet(object):
collect_node_data(tree.root_node, name_to_uuid, all_states, name_to_extra)
has_model = tree.root_node.res_content.type != "deck"
plr_dict = node_to_plr_dict(tree.root_node, has_model)
plr_dict = _deduplicate_plr_dict(plr_dict)
try:
sub_cls = find_subclass(plr_dict["type"], PLRResource)
if skip_devices and plr_dict["type"] == "device":
@@ -667,14 +640,6 @@ class ResourceTreeSet(object):
location = cast(Coordinate, deserialize(plr_dict["location"]))
plr_resource.location = location
# 预填 Container 类型资源在新版 PLR 中要求必须存在的键,
# 防止旧数据库状态缺失这些键时 load_all_state 抛出 KeyError。
for state in all_states.values():
if isinstance(state, dict):
state.setdefault("liquid_history", [])
state.setdefault("pending_liquids", {})
plr_resource.load_all_state(all_states)
# 使用 DeviceNodeResourceTracker 设置 UUID 和 Extra
tracker.loop_set_uuid(plr_resource, name_to_uuid)

View File

@@ -41,9 +41,8 @@ def warehouse_factory(
# 根据 layout 决定 y 坐标计算
if layout == "row-major":
# 行优先row=0(A行) 应该显示在上方
# 前端现在 y 越大越靠上,所以 row=0 对应最大的 y
y = dy + (num_items_y - row - 1) * item_dy
# 行优先row=0(A行) 应该显示在上方,需要较小的 y 值
y = dy + row * item_dy
elif layout == "vertical-col-major":
# 竖向warehouse: row=0 对应顶部y小row=n-1 对应底部y大
# 但标签 01 应该在底部,所以使用反向映射

View File

@@ -45,6 +45,7 @@ from unilabos.resources.graphio import (
)
from unilabos.resources.plr_additional_res_reg import register
from unilabos.ros.msgs.message_converter import (
String,
convert_to_ros_msg,
convert_from_ros_msg_with_mapping,
convert_to_ros_msg_with_mapping,
@@ -250,7 +251,8 @@ class PropertyPublisher:
):
self.node = node
self.name = name
self.msg_type = msg_type
self.msg_type = self._normalize_msg_type(msg_type)
self.original_msg_type = msg_type
self.get_method = get_method
self.timer_period = initial_period
self.print_publish = print_publish
@@ -258,16 +260,36 @@ class PropertyPublisher:
self._value = None
try:
self.publisher_ = node.create_publisher(msg_type, f"{name}", qos)
self.publisher_ = node.create_publisher(self.msg_type, f"{name}", qos)
except Exception as e:
self.node.lab_logger().error(
f"StatusError, DeviceId: {self.node.device_id} 创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {e}"
f"StatusError, DeviceId: {self.node.device_id} 创建发布者 {name} 失败,"
f"可能由于注册表有误,类型: {msg_type},错误: {e}"
)
self.msg_type = String
try:
self.publisher_ = node.create_publisher(self.msg_type, f"{name}", qos)
self.node.lab_logger().warning(
f"属性 {name} 的发布类型已降级为 String原始类型: {msg_type}"
)
except Exception:
self.publisher_ = None
self.timer = node.create_timer(self.timer_period, self.publish_property)
self.__loop = ROS2DeviceNode.get_asyncio_loop()
str_msg_type = str(msg_type)[8:-2]
str_msg_type = str(self.msg_type)[8:-2]
self.node.lab_logger().trace(f"发布属性: {name}, 类型: {str_msg_type}, 周期: {initial_period}秒, QoS: {qos}")
@staticmethod
def _normalize_msg_type(msg_type):
if msg_type in (dict, list, tuple, set) or msg_type in ("dict", "list", "tuple", "set"):
return String
return msg_type
def _normalize_value(self, value):
if self.msg_type is String and isinstance(value, (dict, list, tuple, set)):
return json.dumps(value, ensure_ascii=False, cls=TypeEncoder)
return value
def get_property(self):
if asyncio.iscoroutinefunction(self.get_method):
# 如果是异步函数,运行事件循环并等待结果
@@ -302,12 +324,16 @@ class PropertyPublisher:
pass
# self.node.lab_logger().trace(f"【.publish_property】发布 {self.msg_type}: {value}")
if value is not None:
if self.publisher_ is None:
return
value = self._normalize_value(value)
msg = convert_to_ros_msg(self.msg_type, value)
self.publisher_.publish(msg)
# self.node.lab_logger().trace(f"【.publish_property】属性 {self.name} 发布成功")
except Exception as e:
topic = getattr(self.publisher_, "topic", self.name)
self.node.lab_logger().error(
f"【.publish_property】发布属性 {self.publisher_.topic} 出错: {str(e)}\n{traceback.format_exc()}"
f"【.publish_property】发布属性 {topic} 出错: {str(e)}\n{traceback.format_exc()}"
)
def change_frequency(self, period):
@@ -2340,4 +2366,4 @@ class DeviceInfoType(TypedDict):
status_publishers: Dict[str, PropertyPublisher]
actions: Dict[str, ActionServer]
hardware_interface: Dict[str, Any]
base_node_instance: BaseROS2DeviceNode
base_node_instance: BaseROS2DeviceNode

View File

@@ -1691,7 +1691,9 @@ class HostNode(BaseROS2DeviceNode):
else:
self.lab_logger().warning("⚠️ 收到无效的Pong响应缺少ping_id")
def notify_resource_tree_update(self, device_id: str, action: str, resource_uuid_list: List[str]) -> bool:
def notify_resource_tree_update(
self, device_id: str, action: str, resource_uuid_list: List[str]
) -> Optional[bool]:
"""
通知设备节点更新资源树
@@ -1701,13 +1703,14 @@ class HostNode(BaseROS2DeviceNode):
resource_uuid_list: 资源UUIDs
Returns:
bool: 操作是否成功
True if the update completed, False if it failed, None if it was intentionally skipped.
"""
try:
# 检查设备是否存在
if device_id not in self.devices_names:
self.lab_logger().error(f"[Host Node-Resource] Device {device_id} not found in devices_names")
return False
self.lab_logger().info(
f"[Host Node-Resource] 在线增加设备暂不支持,跳过设备 {device_id} 的资源树 {action} 更新"
)
return None
namespace = self.devices_names[device_id]
device_key = f"{namespace}/{device_id}"

View File

@@ -13,7 +13,7 @@
"deck": {
"data": {
"_resource_child_name": "YB_Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BioyondElectrolyteDeck"
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
}
},
"protocol_type": [],
@@ -103,14 +103,15 @@
"children": [],
"parent": "bioyond_cell_workstation",
"type": "deck",
"class": "BioyondElectrolyteDeck",
"class": "BIOYOND_YB_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BioyondElectrolyteDeck",
"type": "BIOYOND_YB_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,

View File

@@ -33,10 +33,83 @@ _USE_UV: Optional[bool] = None
def _has_uv() -> bool:
global _USE_UV
if _USE_UV is None:
_USE_UV = shutil.which("uv") is not None
uv_path = shutil.which("uv")
if not uv_path:
_USE_UV = False
else:
try:
result = subprocess.run([uv_path, "--version"], capture_output=True, text=True, timeout=10)
_USE_UV = result.returncode == 0
except Exception:
_USE_UV = False
return _USE_UV
def _install_command(installer: str, package: str, upgrade: bool, is_chinese: bool) -> List[str]:
if installer == "uv":
# uv >= 0.5 默认要求虚拟环境,对 conda env 会报 "No virtual environment found"。
# 显式 --python sys.executable 让 uv 把当前解释器conda/venv/system 都行)
# 视为目标环境,绕开 venv 检测。
cmd = ["uv", "pip", "install", "--python", sys.executable]
if upgrade:
cmd.append("--upgrade")
cmd.append(package)
if is_chinese:
cmd.extend(["--index-url", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
return cmd
cmd = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check"]
if upgrade:
cmd.append("--upgrade")
cmd.append(package)
if is_chinese:
cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
return cmd
def _installer_candidates() -> List[str]:
installers: List[str] = []
if _has_uv():
installers.append("uv")
installers.append("pip")
return installers
def _git_url_from_requirement(requirement: str) -> Optional[str]:
if not requirement.startswith("git+"):
return None
return requirement[4:].split("#", 1)[0]
def _repo_dir_name(git_url: str) -> str:
repo_name = git_url.rstrip("/").rsplit("/", 1)[-1]
return repo_name[:-4] if repo_name.endswith(".git") else repo_name
def _print_manual_git_install_hint(requirement: str) -> None:
git_url = _git_url_from_requirement(requirement)
if not git_url:
return
repo_dir = _repo_dir_name(git_url)
install_cmd = (
f'uv pip install --python "{sys.executable}" -e .'
if _has_uv()
else f"{sys.executable} -m pip install -e ."
)
if _is_chinese_locale() and not _has_uv():
install_cmd += " -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
print_status("Git 依赖自动安装失败,通常是网络连接被重置或代码托管站点暂时不可达。", "warning")
print_status("可以手动拉取代码后在本地安装:", "warning")
print_status(f" git clone {git_url}", "warning")
print_status(f" cd {repo_dir}", "warning")
print_status(" git pull", "warning")
print_status(f" {install_cmd}", "warning")
print_status(f"如果目录 {repo_dir} 已存在,直接进入该目录执行 git pull 后再安装。", "warning")
print_status("如果 git clone 仍失败,请切换网络/代理,或从浏览器下载源码后进入源码目录执行本地安装命令。", "warning")
def _install_packages(
packages: List[str],
upgrade: bool = False,
@@ -53,7 +126,7 @@ def _install_packages(
return True
is_chinese = _is_chinese_locale()
use_uv = _has_uv()
installers = _installer_candidates()
failed: List[str] = []
for pkg in packages:
@@ -63,35 +136,30 @@ def _install_packages(
else:
print_status(f"正在{action_word} {pkg}...", "info")
if use_uv:
cmd = ["uv", "pip", "install"]
if upgrade:
cmd.append("--upgrade")
cmd.append(pkg)
if is_chinese:
cmd.extend(["--index-url", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
else:
cmd = [sys.executable, "-m", "pip", "install"]
if upgrade:
cmd.append("--upgrade")
cmd.append(pkg)
if is_chinese:
cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
pkg_installed = False
last_error = "unknown error"
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
installer = "uv" if use_uv else "pip"
print_status(f"{pkg} {action_word}成功 (via {installer})", "success")
else:
stderr_short = result.stderr.strip().split("\n")[-1] if result.stderr else "unknown error"
print_status(f"× {pkg} {action_word}失败: {stderr_short}", "error")
failed.append(pkg)
except subprocess.TimeoutExpired:
print_status(f"× {pkg} {action_word}超时 (300s)", "error")
failed.append(pkg)
except Exception as e:
print_status(f"× {pkg} {action_word}异常: {e}", "error")
for installer in installers:
cmd = _install_command(installer, pkg, upgrade, is_chinese)
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
print_status(f"{pkg} {action_word}成功 (via {installer})", "success")
pkg_installed = True
break
last_error = result.stderr.strip().split("\n")[-1] if result.stderr else "unknown error"
print_status(f"× {pkg} {action_word}失败 (via {installer}): {last_error}", "warning")
except subprocess.TimeoutExpired:
last_error = "timeout after 300s"
print_status(f"× {pkg} {action_word}超时 (via {installer}, 300s)", "warning")
except Exception as e:
last_error = str(e)
print_status(f"× {pkg} {action_word}异常 (via {installer}): {e}", "warning")
if not pkg_installed:
print_status(f"× {pkg} {action_word}失败: {last_error}", "error")
_print_manual_git_install_hint(pkg)
failed.append(pkg)
if failed:
@@ -188,7 +256,13 @@ class EnvironmentChecker:
"crcmod": "crcmod-plus",
}
self.special_packages = {"pylabrobot": "git+https://github.com/Xuwznln/pylabrobot.git"}
# 中文 locale 下走 Gitee 镜像,规避 GitHub 拉取失败
pylabrobot_url = (
"git+https://gitee.com/xuwznln/pylabrobot.git"
if _is_chinese_locale()
else "git+https://github.com/Xuwznln/pylabrobot.git"
)
self.special_packages = {"pylabrobot": pylabrobot_url}
self.version_requirements = {
"msgcenterpy": "0.1.8",

View File

@@ -206,6 +206,7 @@ class ImportManager:
"ast_analysis_success": False,
"import_map": {},
"init_params": [],
"init_docstring": None,
"status_methods": {},
"action_methods": {},
}
@@ -251,6 +252,7 @@ class ImportManager:
# 映射到统一字段名(与 registry.py complete_registry 消费端一致)
result["init_params"] = body.get("init_params", [])
result["init_docstring"] = body.get("init_docstring")
result["status_methods"] = body.get("status_properties", {})
result["action_methods"] = {
k: {

View File

@@ -1,385 +0,0 @@
import logging
import os
import platform
from datetime import datetime
import ctypes
import atexit
import inspect
from typing import Tuple, cast
# 添加TRACE级别到logging模块
TRACE_LEVEL = 5
logging.addLevelName(TRACE_LEVEL, "TRACE")
class CustomRecord:
custom_stack_info: Tuple[str, int, str, str]
# Windows颜色支持
if platform.system() == "Windows":
# 尝试启用Windows终端的ANSI支持
kernel32 = ctypes.windll.kernel32
# 获取STD_OUTPUT_HANDLE
STD_OUTPUT_HANDLE = -11
# 启用ENABLE_VIRTUAL_TERMINAL_PROCESSING
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
# 获取当前控制台模式
handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
mode = ctypes.c_ulong()
kernel32.GetConsoleMode(handle, ctypes.byref(mode))
# 启用ANSI处理
kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
# 程序退出时恢复控制台设置
@atexit.register
def reset_console():
kernel32.SetConsoleMode(handle, mode.value)
# 定义不同日志级别的颜色
class ColoredFormatter(logging.Formatter):
"""自定义日志格式化器,支持颜色输出"""
# ANSI 颜色代码
COLORS = {
"RESET": "\033[0m", # 重置
"BOLD": "\033[1m", # 加粗
"GRAY": "\033[37m", # 灰色
"WHITE": "\033[97m", # 白色
"BLACK": "\033[30m", # 黑色
"TRACE_LEVEL": "\033[1;90m", # 加粗深灰色
"DEBUG_LEVEL": "\033[1;36m", # 加粗青色
"INFO_LEVEL": "\033[1;32m", # 加粗绿色
"WARNING_LEVEL": "\033[1;33m", # 加粗黄色
"ERROR_LEVEL": "\033[1;31m", # 加粗红色
"CRITICAL_LEVEL": "\033[1;35m", # 加粗紫色
"TRACE_TEXT": "\033[90m", # 深灰色
"DEBUG_TEXT": "\033[37m", # 灰色
"INFO_TEXT": "\033[97m", # 白色
"WARNING_TEXT": "\033[33m", # 黄色
"ERROR_TEXT": "\033[31m", # 红色
"CRITICAL_TEXT": "\033[35m", # 紫色
"DATE": "\033[37m", # 日期始终使用灰色
}
def __init__(self, use_colors=True):
super().__init__()
# 强制启用颜色
self.use_colors = use_colors
def format(self, record):
# 检查是否有自定义堆栈信息
if hasattr(record, "custom_stack_info") and record.custom_stack_info: # type: ignore
r = cast(CustomRecord, record)
frame_info = r.custom_stack_info
record.filename = frame_info[0]
record.lineno = frame_info[1]
record.funcName = frame_info[2]
if len(frame_info) > 3:
record.name = frame_info[3]
if not self.use_colors:
return self._format_basic(record)
level_color = self.COLORS.get(f"{record.levelname}_LEVEL", self.COLORS["WHITE"])
text_color = self.COLORS.get(f"{record.levelname}_TEXT", self.COLORS["WHITE"])
date_color = self.COLORS["DATE"]
reset = self.COLORS["RESET"]
# 日期格式
datetime_str = datetime.fromtimestamp(record.created).strftime("%y-%m-%d [%H:%M:%S,%f")[:-3] + "]"
# 模块和函数信息
filename = record.filename.replace(".py", "").split("\\")[-1] # 提取文件名(不含路径和扩展名)
if "/" in filename:
filename = filename.split("/")[-1]
module_path = f"{record.name}.{filename}"
func_line = f"{record.funcName}:{record.lineno}"
right_info = f" [{func_line}] [{module_path}]"
# 主要消息
main_msg = record.getMessage()
# 构建基本消息格式
formatted_message = (
f"{date_color}{datetime_str}{reset} "
f"{level_color}[{record.levelname}]{reset} "
f"{text_color}{main_msg}"
f"{date_color}{right_info}{reset}"
)
# 处理异常信息
if record.exc_info:
exc_text = self.formatException(record.exc_info)
if formatted_message[-1:] != "\n":
formatted_message = formatted_message + "\n"
formatted_message = formatted_message + text_color + exc_text + reset
elif record.stack_info:
if formatted_message[-1:] != "\n":
formatted_message = formatted_message + "\n"
formatted_message = formatted_message + text_color + self.formatStack(record.stack_info) + reset
return formatted_message
def _format_basic(self, record):
"""基本格式化,不包含颜色"""
datetime_str = datetime.fromtimestamp(record.created).strftime("%y-%m-%d [%H:%M:%S,%f")[:-3] + "]"
filename = record.filename.replace(".py", "").split("\\")[-1] # 提取文件名(不含路径和扩展名)
if "/" in filename:
filename = filename.split("/")[-1]
module_path = f"{record.name}.{filename}"
func_line = f"{record.funcName}:{record.lineno}"
right_info = f" [{func_line}] [{module_path}]"
formatted_message = f"{datetime_str} [{record.levelname}] {record.getMessage()}{right_info}"
if record.exc_info:
exc_text = self.formatException(record.exc_info)
if formatted_message[-1:] != "\n":
formatted_message = formatted_message + "\n"
formatted_message = formatted_message + exc_text
elif record.stack_info:
if formatted_message[-1:] != "\n":
formatted_message = formatted_message + "\n"
formatted_message = formatted_message + self.formatStack(record.stack_info)
return formatted_message
def formatException(self, exc_info):
"""重写异常格式化,确保异常信息保持正确的格式和颜色"""
# 获取标准的异常格式化文本
formatted_exc = super().formatException(exc_info)
return formatted_exc
# 配置日志处理器
def configure_logger(loglevel=None, working_dir=None):
"""配置日志记录器
Args:
loglevel: 日志级别,可以是字符串('TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
或logging模块的常量如logging.DEBUG或TRACE_LEVEL
"""
# 获取根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(TRACE_LEVEL)
# 设置日志级别
numeric_level = logging.DEBUG
if loglevel is not None:
if isinstance(loglevel, str):
# 将字符串转换为logging级别
if loglevel.upper() == "TRACE":
numeric_level = TRACE_LEVEL
else:
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
print(f"警告: 无效的日志级别 '{loglevel}',使用默认级别 DEBUG")
else:
numeric_level = loglevel
# 移除已存在的处理器
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(numeric_level) # 使用与根记录器相同的级别
# 使用自定义的颜色格式化器
color_formatter = ColoredFormatter()
console_handler.setFormatter(color_formatter)
# 添加处理器到根日志记录器
root_logger.addHandler(console_handler)
# 如果指定了工作目录,添加文件处理器
if working_dir is not None:
logs_dir = os.path.join(working_dir, "logs")
os.makedirs(logs_dir, exist_ok=True)
# 生成日志文件名:日期 时间.log
log_filename = datetime.now().strftime("%Y-%m-%d %H-%M-%S") + ".log"
log_filepath = os.path.join(logs_dir, log_filename)
# 创建文件处理器
file_handler = logging.FileHandler(log_filepath, encoding="utf-8")
file_handler.setLevel(TRACE_LEVEL)
# 使用不带颜色的格式化器
file_formatter = ColoredFormatter(use_colors=False)
file_handler.setFormatter(file_formatter)
root_logger.addHandler(file_handler)
logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("urllib3").setLevel(logging.INFO)
# 配置日志系统
configure_logger()
# 获取日志记录器
logger = logging.getLogger(__name__)
# 获取调用栈信息的工具函数
def _get_caller_info(stack_level=0) -> Tuple[str, int, str, str]:
"""
获取调用者的信息
Args:
stack_level: 堆栈回溯的级别0表示当前函数1表示调用者依此类推
Returns:
(filename, line_number, function_name, module_name) 元组
"""
# 堆栈级别需要加3:
# +1 因为这个函数本身占一层
# +1 因为日志函数(debug, info等)占一层
# +1 因为下面调用 inspect.stack() 也占一层
frame = inspect.currentframe()
try:
# 跳过适当的堆栈帧
for _ in range(stack_level + 3):
if frame and frame.f_back:
frame = frame.f_back
else:
break
if frame:
filename = frame.f_code.co_filename if frame.f_code else "unknown"
line_number = frame.f_lineno if hasattr(frame, "f_lineno") else 0
function_name = frame.f_code.co_name if frame.f_code else "unknown"
# 获取模块名称
module_name = "unknown"
if frame.f_globals and "__name__" in frame.f_globals:
module_name = frame.f_globals["__name__"].rsplit(".", 1)[0]
return (filename, line_number, function_name, module_name)
return ("unknown", 0, "unknown", "unknown")
finally:
del frame # 避免循环引用
# 便捷日志记录函数
def debug(msg, *args, stack_level=0, **kwargs):
"""
记录DEBUG级别日志
Args:
msg: 日志消息
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
*args, **kwargs: 传递给logger.debug的其他参数
"""
# 获取调用者信息
if stack_level > 0:
caller_info = _get_caller_info(stack_level)
extra = kwargs.get("extra", {})
extra["custom_stack_info"] = caller_info
kwargs["extra"] = extra
logger.debug(msg, *args, **kwargs)
def info(msg, *args, stack_level=0, **kwargs):
"""
记录INFO级别日志
Args:
msg: 日志消息
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
*args, **kwargs: 传递给logger.info的其他参数
"""
if stack_level > 0:
caller_info = _get_caller_info(stack_level)
extra = kwargs.get("extra", {})
extra["custom_stack_info"] = caller_info
kwargs["extra"] = extra
logger.info(msg, *args, **kwargs)
def warning(msg, *args, stack_level=0, **kwargs):
"""
记录WARNING级别日志
Args:
msg: 日志消息
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
*args, **kwargs: 传递给logger.warning的其他参数
"""
if stack_level > 0:
caller_info = _get_caller_info(stack_level)
extra = kwargs.get("extra", {})
extra["custom_stack_info"] = caller_info
kwargs["extra"] = extra
logger.warning(msg, *args, **kwargs)
def error(msg, *args, stack_level=0, **kwargs):
"""
记录ERROR级别日志
Args:
msg: 日志消息
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
*args, **kwargs: 传递给logger.error的其他参数
"""
if stack_level > 0:
caller_info = _get_caller_info(stack_level)
extra = kwargs.get("extra", {})
extra["custom_stack_info"] = caller_info
kwargs["extra"] = extra
logger.error(msg, *args, **kwargs)
def critical(msg, *args, stack_level=0, **kwargs):
"""
记录CRITICAL级别日志
Args:
msg: 日志消息
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
*args, **kwargs: 传递给logger.critical的其他参数
"""
if stack_level > 0:
caller_info = _get_caller_info(stack_level)
extra = kwargs.get("extra", {})
extra["custom_stack_info"] = caller_info
kwargs["extra"] = extra
logger.critical(msg, *args, **kwargs)
def trace(msg, *args, stack_level=0, **kwargs):
"""
记录TRACE级别日志比DEBUG级别更低
Args:
msg: 日志消息
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
*args, **kwargs: 传递给logger.log的其他参数
"""
if stack_level > 0:
caller_info = _get_caller_info(stack_level)
extra = kwargs.get("extra", {})
extra["custom_stack_info"] = caller_info
kwargs["extra"] = extra
logger.log(TRACE_LEVEL, msg, *args, **kwargs)
logger.trace = trace
# 测试日志输出(如果直接运行此文件)
if __name__ == "__main__":
print("测试不同日志级别的颜色输出:")
trace("这是一条跟踪日志 (TRACE级别显示为深灰色其他文本也为深灰色)")
debug("这是一条调试日志 (DEBUG级别显示为蓝色其他文本为灰色)")
info("这是一条信息日志 (INFO级别显示为绿色其他文本为白色)")
warning("这是一条警告日志 (WARNING级别显示为黄色其他文本也为黄色)")
error("这是一条错误日志 (ERROR级别显示为红色其他文本也为红色)")
critical("这是一条严重错误日志 (CRITICAL级别显示为紫色其他文本也为紫色)")
# 测试异常输出
try:
1 / 0
except Exception as e:
error(f"发生错误: {e}", exc_info=True)

View File

@@ -191,23 +191,9 @@ def configure_logger(loglevel=None, working_dir=None):
# 添加处理器到根日志记录器
root_logger.addHandler(console_handler)
# 降低第三方库的日志级别,避免过多输出
# pymodbus 库的日志太详细,设置为 WARNING
logging.getLogger('pymodbus').setLevel(logging.WARNING)
logging.getLogger('pymodbus.logging').setLevel(logging.WARNING)
logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING)
logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING)
# websockets 库的日志输出较多,设置为 WARNING
logging.getLogger('websockets').setLevel(logging.WARNING)
logging.getLogger('websockets.client').setLevel(logging.WARNING)
logging.getLogger('websockets.server').setLevel(logging.WARNING)
# ROS 节点的状态更新日志过于频繁,设置为 INFO
logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.INFO)
# 如果指定了工作目录,添加文件处理器
log_filepath = None
if working_dir is not None:
logs_dir = os.path.join(working_dir, "logs")
os.makedirs(logs_dir, exist_ok=True)
@@ -228,6 +214,7 @@ def configure_logger(loglevel=None, working_dir=None):
logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("urllib3").setLevel(logging.INFO)
return log_filepath
# 配置日志系统

View File

@@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>unilabos_msgs</name>
<version>0.10.19</version>
<version>0.11.3</version>
<description>ROS2 Messages package for unilabos devices</description>
<maintainer email="changjh@pku.edu.cn">Junhan Chang</maintainer>
<maintainer email="18435084+Xuwznln@users.noreply.github.com">Xuwznln</maintainer>