# 变更说明 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-25:3→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 个资源全部无法放置到对应仓库位,前端不显示。