diff --git a/.gitignore b/.gitignore index 838331e3..3a52c0e4 100644 --- a/.gitignore +++ b/.gitignore @@ -250,4 +250,7 @@ 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 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-1.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-1.xlsx new file mode 100644 index 00000000..52a18b73 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-1.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-2.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-2.xlsx new file mode 100644 index 00000000..6243a4d6 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-2.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-3.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-3.xlsx new file mode 100644 index 00000000..6749fa85 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-3.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-4.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-4.xlsx new file mode 100644 index 00000000..d303b71f Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-4.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260321-5.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260321-5.xlsx new file mode 100644 index 00000000..eecad6f6 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260321-5.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323-1.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323-1.xlsx new file mode 100644 index 00000000..d2c41c5a Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323-1.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323.xlsx new file mode 100644 index 00000000..7c56216a Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template2.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template2.xlsx new file mode 100644 index 00000000..9fb8bdbd Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template2.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template3.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template3.xlsx new file mode 100644 index 00000000..cf463a3a Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template3.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template4.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template4.xlsx new file mode 100644 index 00000000..1afec460 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template4.xlsx differ diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py index 83c7f598..dbb05e8c 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -1111,6 +1111,42 @@ 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: diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_20260112.xlsx b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_20260112.xlsx new file mode 100644 index 00000000..40a421b5 Binary files /dev/null and b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_20260112.xlsx differ diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260317.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260317.csv new file mode 100644 index 00000000..7468dbcb --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260317.csv @@ -0,0 +1,10 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code,formulation_order_code,formulation_ratio +20260317_162514,0.6470000147819519,28.75,502.0,3318,80,7,NoRead88,YS104219,, +20260317_163955,0.6800000071525574,28.780000686645508,883.0,3285,80,7,NoRead88,YS104395,, +20260317_171603,1.1490000486373901,0.0,2167.0,3302,80,7,NoRead88,YS104287,, +20260317_172257,1.3760000467300415,28.10999870300293,414.0,3269,80,7,NoRead88,YS104286,, +20260317_173332,3.171999931335449,28.84000015258789,634.0,3318,80,7,NoRead88,17160106,, +20260317_173614,3.0429999828338623,28.75,161.0,3285,80,7,NoRead88,YS104389,, +20260317_173856,3.140000104904175,28.579998016357422,160.0,3205,80,7,NoRead88,YS104357,, +20260317_174428,3.171999931335449,28.06999969482422,318.0,3285,80,7,NoRead88,YS104212,, +20260317_180440,3.171999931335449,28.44999885559082,200.0,3269,80,7,NoRead88,YS104228,, diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260319.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260319.csv new file mode 100644 index 00000000..86b1133c --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260319.csv @@ -0,0 +1,2 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code,formulation_order_code,formulation_ratio +20260319_114636,0.2590000033378601,27.529996871948242,258.0,3302,60,7,NoRead88,YS104373,, diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260323.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260323.csv new file mode 100644 index 00000000..60b16c27 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260323.csv @@ -0,0 +1,7 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code,formulation_order_code,formulation_ratio +20260323_144656,0.01600000075995922,12.25999927520752,220.0,3334,20,7,NoRead88,14441891,, +20260323_144929,0.0,11.940000534057617,152.0,3075,20,7,NoRead88,14465181,, +20260323_145329,0.0,12.229999542236328,160.0,3269,20,7,NoRead88,14492431,, +20260323_145726,0.0,12.34999942779541,316.0,3367,20,7,NoRead88,14544961,, +20260323_150000,0.0,12.100000381469727,152.0,3269,20,7,NoRead88,14572221,, +20260323_150514,0.0,12.49000072479248,314.0,3237,20,7,NoRead88,14595521,, diff --git a/unilabos/devices/workstation/implementation_plan.md b/unilabos/devices/workstation/implementation_plan.md new file mode 100644 index 00000000..86f2ee32 --- /dev/null +++ b/unilabos/devices/workstation/implementation_plan.md @@ -0,0 +1,88 @@ +# 物料系统标准化重构方案 + +根据开发者的反馈,本方案旨在遵循“标准化而非绕过”的原则,对资源类(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 报错。 + +### 手动验证 +* 重启各工作站节点,验证资源树是否能根据数据库数据正确还还原。 +* 验证“自动”与“手动”传输窗资源在台面上的分配是否正确。 diff --git a/unilabos/devices/workstation/implementation_plan_v2.md b/unilabos/devices/workstation/implementation_plan_v2.md new file mode 100644 index 00000000..7e2233ef --- /dev/null +++ b/unilabos/devices/workstation/implementation_plan_v2.md @@ -0,0 +1,388 @@ +# 物料系统标准化重构方案 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 — 移除瓶子填充 + +阶段 B(Deck 层) + 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` 目标查找方式 | +| 验证计划 | 简述目标 | 提供具体测试命令和逐步手动验证流程 | diff --git a/unilabos/registry/resources/battery/bottle_carriers.yaml b/unilabos/registry/resources/battery/bottle_carriers.yaml new file mode 100644 index 00000000..d004ee8b --- /dev/null +++ b/unilabos/registry/resources/battery/bottle_carriers.yaml @@ -0,0 +1,12 @@ +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 diff --git a/unilabos/registry/resources/bioyond/YB_bottle.yaml b/unilabos/registry/resources/bioyond/YB_bottle.yaml index f8e17261..b339e0d8 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle.yaml @@ -1,90 +1,146 @@ -YB_20ml_fenyeping: +YB_Vial_20mL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping + module: unilabos.resources.bioyond.YB_bottles:YB_Vial_20mL type: pylabrobot - description: YB_20ml_fenyeping + description: YB_Vial_20mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_5ml_fenyeping: +YB_Vial_5mL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping + module: unilabos.resources.bioyond.YB_bottles:YB_Vial_5mL type: pylabrobot - description: YB_5ml_fenyeping + description: YB_Vial_5mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_jia_yang_tou_da: +YB_DosingHead_L: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da + module: unilabos.resources.bioyond.YB_bottles:YB_DosingHead_L type: pylabrobot - description: YB_jia_yang_tou_da + description: YB_DosingHead_L handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_pei_ye_da_Bottle: +YB_PrepBottle_60mL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle + module: unilabos.resources.bioyond.YB_bottles:YB_PrepBottle_60mL type: pylabrobot - description: YB_pei_ye_da_Bottle + description: YB_PrepBottle_60mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_pei_ye_xiao_Bottle: +YB_PrepBottle_15mL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle + module: unilabos.resources.bioyond.YB_bottles:YB_PrepBottle_15mL type: pylabrobot - description: YB_pei_ye_xiao_Bottle + description: YB_PrepBottle_15mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_qiang_tou: +YB_Tip_5000uL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou + module: unilabos.resources.bioyond.YB_bottles:YB_Tip_5000uL type: pylabrobot - description: YB_qiang_tou + description: YB_Tip_5000uL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_ye_Bottle: +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: category: - - yb3 - YB_bottle_carriers - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle + module: unilabos.resources.bioyond.YB_bottles:YB_NormalLiq_250mL_Bottle type: pylabrobot - description: YB_ye_Bottle + 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 handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml index 4698a266..c352d0f2 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml @@ -1,37 +1,22 @@ -YB_100ml_yeti: +YB_Vial_20mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Vial_20mL_Carrier type: pylabrobot - description: YB_100ml_yeti + description: YB_Vial_20mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_20ml_fenyepingban: +YB_Vial_5mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Vial_5mL_Carrier type: pylabrobot - description: YB_20ml_fenyepingban - handles: [] - icon: '' - init_param_schema: {} - registry_type: resource - version: 1.0.0 -YB_5ml_fenyepingban: - category: - - yb3 - - YB_bottle_carriers - class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban - type: pylabrobot - description: YB_5ml_fenyepingban + description: YB_Vial_5mL_Carrier handles: [] icon: '' init_param_schema: {} @@ -39,7 +24,6 @@ YB_5ml_fenyepingban: version: 1.0.0 YB_6StockCarrier: category: - - yb3 - YB_bottle_carriers class: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier @@ -52,7 +36,6 @@ YB_6StockCarrier: version: 1.0.0 YB_6VialCarrier: category: - - yb3 - YB_bottle_carriers class: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier @@ -63,120 +46,135 @@ YB_6VialCarrier: init_param_schema: {} registry_type: resource version: 1.0.0 -YB_gao_nian_ye_Bottle: +YB_DosingHead_L_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_DosingHead_L_Carrier type: pylabrobot - description: YB_gao_nian_ye_Bottle + description: YB_DosingHead_L_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_gaonianye: +YB_PrepBottle_60mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_PrepBottle_60mL_Carrier type: pylabrobot - description: YB_gaonianye + description: YB_PrepBottle_60mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_jia_yang_tou_da_Carrier: +YB_PrepBottle_15mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_PrepBottle_15mL_Carrier type: pylabrobot - description: YB_jia_yang_tou_da_Carrier + description: YB_PrepBottle_15mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_peiyepingdaban: +YB_TipRack_Mixed: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_Mixed type: pylabrobot - description: YB_peiyepingdaban + description: YB_TipRack_Mixed handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_peiyepingxiaoban: +YB_TipRack_5000uL: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_5000uL type: pylabrobot - description: YB_peiyepingxiaoban + description: YB_TipRack_5000uL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_qiang_tou_he: +YB_TipRack_50uL: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_50uL type: pylabrobot - description: YB_qiang_tou_he + description: YB_TipRack_50uL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_shi_pei_qi_kuai: +YB_Adapter_60mL: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Adapter_60mL type: pylabrobot - description: YB_shi_pei_qi_kuai + description: YB_Adapter_60mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_ye: +YB_NormalLiq_250mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_NormalLiq_250mL_Carrier type: pylabrobot - description: YB_ye_Bottle_Carrier + description: YB_NormalLiq_250mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_ye_100ml_Bottle: +YB_NormalLiq_100mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_NormalLiq_100mL_Carrier type: pylabrobot - description: YB_ye_100ml_Bottle + description: YB_NormalLiq_100mL_Carrier 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 \ No newline at end of file diff --git a/unilabos/resources/bioyond/YB_bottle_carriers.py b/unilabos/resources/bioyond/YB_bottle_carriers.py index 29a53242..3add4f79 100644 --- a/unilabos/resources/bioyond/YB_bottle_carriers.py +++ b/unilabos/resources/bioyond/YB_bottle_carriers.py @@ -2,15 +2,18 @@ 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_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, + 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, ) # 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial @@ -206,7 +209,7 @@ def YB_6VialCarrier(name: str) -> BottleCarrier: return carrier # 1瓶载架 - 单个中央位置 -def YB_ye(name: str) -> BottleCarrier: +def YB_NormalLiq_250mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -233,17 +236,17 @@ def YB_ye(name: str) -> BottleCarrier: resource_size_y=beaker_diameter, name_prefix=name, ), - model="YB_ye", + model="YB_NormalLiq_250mL_Carrier", ) carrier.num_items_x = 1 carrier.num_items_y = 1 carrier.num_items_z = 1 - carrier[0] = YB_ye_Bottle(f"{name}_flask_1") + carrier[0] = YB_NormalLiq_250mL_Bottle(f"{name}_flask_1") return carrier # 高粘液瓶载架 - 单个中央位置 -def YB_gaonianye(name: str) -> BottleCarrier: +def YB_HighVis_250mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -270,17 +273,17 @@ def YB_gaonianye(name: str) -> BottleCarrier: resource_size_y=beaker_diameter, name_prefix=name, ), - model="YB_gaonianye", + model="YB_HighVis_250mL_Carrier", ) carrier.num_items_x = 1 carrier.num_items_y = 1 carrier.num_items_z = 1 - carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1") + carrier[0] = YB_HighVis_250mL_Bottle(f"{name}_flask_1") return carrier -# 100ml液体瓶载架 - 单个中央位置 -def YB_100ml_yeti(name: str) -> BottleCarrier: +# 100mL普通液瓶载架 - 单个中央位置 +def YB_NormalLiq_100mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -307,16 +310,52 @@ def YB_100ml_yeti(name: str) -> BottleCarrier: resource_size_y=beaker_diameter, name_prefix=name, ), - model="YB_100ml_yeti", + model="YB_NormalLiq_100mL_Carrier", ) carrier.num_items_x = 1 carrier.num_items_y = 1 carrier.num_items_z = 1 - carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1") + carrier[0] = YB_NormalLiq_100mL_Bottle(f"{name}_flask_1") return carrier -# 5ml分液瓶板 - 4x2布局,8个位置 -def YB_5ml_fenyepingban(name: str) -> BottleCarrier: +# 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: # 载架尺寸 (mm) @@ -355,18 +394,18 @@ def YB_5ml_fenyepingban(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_5ml_fenyepingban", + model="YB_Vial_5mL_Carrier", ) carrier.num_items_x = 4 carrier.num_items_y = 2 carrier.num_items_z = 1 ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] for i in range(8): - carrier[i] = YB_5ml_fenyeping(f"{name}_vial_{ordering[i]}") + carrier[i] = YB_Vial_5mL(f"{name}_vial_{ordering[i]}") return carrier -# 20ml分液瓶板 - 4x2布局,8个位置 -def YB_20ml_fenyepingban(name: str) -> BottleCarrier: +# 20mL分液瓶板 - 4x2布局,8个位置 +def YB_Vial_20mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) @@ -405,18 +444,18 @@ def YB_20ml_fenyepingban(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_20ml_fenyepingban", + model="YB_Vial_20mL_Carrier", ) carrier.num_items_x = 4 carrier.num_items_y = 2 carrier.num_items_z = 1 ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] for i in range(8): - carrier[i] = YB_20ml_fenyeping(f"{name}_vial_{ordering[i]}") + carrier[i] = YB_Vial_20mL(f"{name}_vial_{ordering[i]}") return carrier # 配液瓶(小)板 - 4x2布局,8个位置 -def YB_peiyepingxiaoban(name: str) -> BottleCarrier: +def YB_PrepBottle_15mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) @@ -455,19 +494,19 @@ def YB_peiyepingxiaoban(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_peiyepingxiaoban", + model="YB_PrepBottle_15mL_Carrier", ) carrier.num_items_x = 4 carrier.num_items_y = 2 carrier.num_items_z = 1 ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] for i in range(8): - carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}") + carrier[i] = YB_PrepBottle_15mL(f"{name}_bottle_{ordering[i]}") return carrier # 配液瓶(大)板 - 2x2布局,4个位置 -def YB_peiyepingdaban(name: str) -> BottleCarrier: +def YB_PrepBottle_60mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -505,18 +544,18 @@ def YB_peiyepingdaban(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_peiyepingdaban", + model="YB_PrepBottle_60mL_Carrier", ) carrier.num_items_x = 2 carrier.num_items_y = 2 carrier.num_items_z = 1 ordering = ["A1", "A2", "B1", "B2"] for i in range(4): - carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}") + carrier[i] = YB_PrepBottle_60mL(f"{name}_bottle_{ordering[i]}") return carrier # 加样头(大)板 - 1x1布局,1个位置 -def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier: +def YB_DosingHead_L_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -554,16 +593,16 @@ def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_jia_yang_tou_da_Carrier", + model="YB_DosingHead_L_Carrier", ) carrier.num_items_x = 1 carrier.num_items_y = 1 carrier.num_items_z = 1 - carrier[0] = YB_jia_yang_tou_da(f"{name}_head_1") + carrier[0] = YB_DosingHead_L(f"{name}_head_1") return carrier -def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier: +def YB_Adapter_60mL(name: str) -> BottleCarrier: """适配器块 - 单个中央位置""" # 载架尺寸 (mm) @@ -591,7 +630,7 @@ def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier: resource_size_y=adapter_diameter, name_prefix=name, ), - model="YB_shi_pei_qi_kuai", + model="YB_Adapter_60mL", ) carrier.num_items_x = 1 carrier.num_items_y = 1 @@ -600,7 +639,7 @@ def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier: return carrier -def YB_qiang_tou_he(name: str) -> BottleCarrier: +def YB_TipRack_50uL(name: str) -> BottleCarrier: """枪头盒 - 8x12布局,96个位置""" # 载架尺寸 (mm) @@ -609,9 +648,9 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier: carrier_size_z = 55.0 # 枪头尺寸 - tip_diameter = 10.0 - tip_spacing_x = 9.0 # X方向间距 - tip_spacing_y = 9.0 # Y方向间距 + tip_diameter = 7.0 + tip_spacing_x = 7.5 # X方向间距 + tip_spacing_y = 7.5 # Y方向间距 # 计算起始位置 (居中排列) start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2 @@ -639,7 +678,7 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_qiang_tou_he", + model="YB_TipRack_50uL", ) carrier.num_items_x = 12 carrier.num_items_y = 8 @@ -648,6 +687,182 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier: for i in range(96): row = chr(65 + i // 12) # A-H col = (i % 12) + 1 # 1-12 - carrier[i] = YB_qiang_tou(f"{name}_tip_{row}{col}") + 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}") + return carrier diff --git a/unilabos/resources/bioyond/YB_bottles.py b/unilabos/resources/bioyond/YB_bottles.py index acbbf35b..54f3e2a9 100644 --- a/unilabos/resources/bioyond/YB_bottles.py +++ b/unilabos/resources/bioyond/YB_bottles.py @@ -1,7 +1,7 @@ from unilabos.resources.itemized_carrier import Bottle, BottleCarrier # 工厂函数 """加样头(大)""" -def YB_jia_yang_tou_da( +def YB_DosingHead_L( name: str, diameter: float = 20.0, height: float = 100.0, @@ -15,11 +15,11 @@ def YB_jia_yang_tou_da( height=height, max_volume=max_volume, barcode=barcode, - model="YB_jia_yang_tou_da", + model="YB_DosingHead_L", ) -"""液1x1""" -def YB_ye_Bottle( +"""250mL普通液""" +def YB_NormalLiq_250mL_Bottle( name: str, diameter: float = 40.0, height: float = 70.0, @@ -33,83 +33,101 @@ def YB_ye_Bottle( height=height, max_volume=max_volume, barcode=barcode, - model="YB_ye_Bottle", + model="YB_NormalLiq_250mL_Bottle", ) -"""100ml液体""" -def YB_ye_100ml_Bottle( +"""100mL普通液""" +def YB_NormalLiq_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_100ml_yeti", + model="YB_NormalLiq_100mL_Bottle", ) -"""高粘液""" -def YB_gao_nian_ye_Bottle( +"""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( 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="High_Viscosity_Liquid", + model="YB_HighVis_250mL_Bottle", ) -"""5ml分液瓶""" -def YB_5ml_fenyeping( +"""5mL分液瓶""" +def YB_Vial_5mL( 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_5ml_fenyeping", + model="YB_Vial_5mL", ) -"""20ml分液瓶""" -def YB_20ml_fenyeping( +"""20mL分液瓶""" +def YB_Vial_20mL( 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_20ml_fenyeping", + model="YB_Vial_20mL", ) """配液瓶(小)""" -def YB_pei_ye_xiao_Bottle( +def YB_PrepBottle_15mL( name: str, diameter: float = 35.0, height: float = 60.0, @@ -123,11 +141,11 @@ def YB_pei_ye_xiao_Bottle( height=height, max_volume=max_volume, barcode=barcode, - model="YB_pei_ye_xiao_Bottle", + model="YB_PrepBottle_15mL", ) """配液瓶(大)""" -def YB_pei_ye_da_Bottle( +def YB_PrepBottle_60mL( name: str, diameter: float = 55.0, height: float = 100.0, @@ -141,11 +159,29 @@ def YB_pei_ye_da_Bottle( height=height, max_volume=max_volume, barcode=barcode, - model="YB_pei_ye_da_Bottle", + model="YB_PrepBottle_60mL", ) -"""枪头""" -def YB_qiang_tou( +"""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( name: str, diameter: float = 10.0, height: float = 50.0, @@ -159,5 +195,23 @@ def YB_qiang_tou( height=height, max_volume=max_volume, barcode=barcode, - model="YB_qiang_tou", + model="YB_Tip_1000uL", ) + +"""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", + ) \ No newline at end of file diff --git a/unilabos/utils/log-origin.py b/unilabos/utils/log-origin.py new file mode 100644 index 00000000..cee3269b --- /dev/null +++ b/unilabos/utils/log-origin.py @@ -0,0 +1,385 @@ +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) diff --git a/unilabos/utils/log.py b/unilabos/utils/log.py index cee3269b..f10bd518 100644 --- a/unilabos/utils/log.py +++ b/unilabos/utils/log.py @@ -191,6 +191,21 @@ 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) # 如果指定了工作目录,添加文件处理器 if working_dir is not None: