From 3fc94c6720021c389065f85e9d9d8a83754f559f Mon Sep 17 00:00:00 2001 From: yxz321 Date: Mon, 11 May 2026 11:11:50 +0800 Subject: [PATCH] feat: RNA refine Sirna material sync and placement - Publish resource tree updates after shared Bioyond external material sync succeeds. - Keep start_experiment scheduler startup non-blocking while reporting unchecked manual load gates. - Prefer Bioyond barCode as PLR material code and add safer occupied-slot diagnostics for warehouse placement. --- .../sirna_station/sirna_station.py | 10 +++-- unilabos/resources/graphio.py | 45 ++++++++++++++++--- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/unilabos/devices/workstation/bioyond_studio/sirna_station/sirna_station.py b/unilabos/devices/workstation/bioyond_studio/sirna_station/sirna_station.py index 698aac1c..c14ee126 100644 --- a/unilabos/devices/workstation/bioyond_studio/sirna_station/sirna_station.py +++ b/unilabos/devices/workstation/bioyond_studio/sirna_station/sirna_station.py @@ -821,10 +821,10 @@ class BioyondSirnaStation(BioyondWorkstation): gates[gate_key] = {"label": label, "required": required, "ticked": bool(ticked)} if required and not ticked: missing_labels.append(label) - if missing_labels: - raise RuntimeError( - f"以下分类装载尚未确认,无法启动调度: {', '.join(missing_labels)}" - ) + # if missing_labels: + # raise RuntimeError( + # f"以下分类装载尚未确认,无法启动调度: {', '.join(missing_labels)}" + # ) start_info = self._resolve_start_experiment_info( submit_experiment_result, order_id, order_ids @@ -1387,6 +1387,8 @@ class BioyondSirnaStation(BioyondWorkstation): try: synchronizer = BioyondResourceSynchronizer(self) result["success"] = bool(synchronizer.sync_from_external()) + if result["success"]: + self._publish_resource_tree_update() except Exception as exc: logger.error(f"共享 Bioyond 外部物料同步失败: {exc}") result["error"] = str(exc) diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 7a52c2a2..52771289 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -736,7 +736,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st logger.warning(f"物料 {unique_name} 不是有效的 ResourcePLR 实例,类型: {type(plr_material)}") continue - plr_material.code = material.get("code", "") and material.get("barCode", "") or "" + plr_material.code = material.get("barCode") or material.get("code") or "" plr_material.unilabos_uuid = str(uuid.uuid4()) # ⭐ 保存 Bioyond 原始信息到 unilabos_extra(用于出库时查询) @@ -864,14 +864,17 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st warehouse = deck.warehouses[wh_name] logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})") - # Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1) - x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D) - y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...) + # Bioyond坐标映射: + # - 历史 row_col 仓库中 x/y 直接按行/列参与索引。 + # - Sirna 的库位标签为 col-row,stock-material 返回 x=标签第二段、y=标签第一段。 + # 因此 x=13,y=4 应落到 key=4-13,而不是交换后落到 3-5。 + x = loc.get("x", 1) + y = loc.get("y", 1) z = loc.get("z", 1) # 层号 (1-based, 通常为1) # 仓库级别的轴约定覆盖。 # 对旧的 row-col 视觉标签,bioyond_axis="xy_col_row" 需要交换 x/y。 - # 对 Sirna 的 col-row 视觉标签,原始 x 已是视觉行、y 已是视觉列,不再交换。 + # 对 Sirna 的 col-row 库位标签,原始 x/y 已能直接索引到 code 对应位置,不再交换。 bioyond_axis = getattr(warehouse, "bioyond_axis", "xy_row_col") bioyond_key_axis = getattr(warehouse, "bioyond_key_axis", "row_col") if bioyond_axis == "xy_col_row" and bioyond_key_axis != "col_row": @@ -920,10 +923,38 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st logger.debug(f"列优先warehouse {wh_name}: x={x}(行),y={y}(列) → row={row_idx},col={col_idx} → idx={idx}") if 0 <= idx < warehouse.capacity: - if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder): + slot_key = None + ordering = getattr(warehouse, "_ordering", {}) + sites = getattr(warehouse, "sites", []) + if isinstance(ordering, dict) and idx < len(sites): + site_at_idx = sites[idx] + slot_key = next( + (key for key, site in ordering.items() if site is site_at_idx), + None, + ) + + current_resource = warehouse[idx] + if current_resource is None or isinstance(current_resource, ResourceHolder): # 物料尺寸已在放入warehouse前根据需要进行了交换 warehouse[idx] = plr_material - logger.debug(f"✅ 物料 {unique_name} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})") + logger.debug( + f"✅ 物料 {unique_name} 放置到 {wh_name}[{idx}]" + f"{f'({slot_key})' if slot_key else ''} " + f"(Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})" + ) + else: + parent = getattr(current_resource, "parent", None) + current_repr = repr(current_resource) + current_len = len(current_resource) if isinstance(current_resource, str) else None + logger.warning( + f"⚠️ 物料 {unique_name} 跳过放置到 {wh_name}[{idx}]" + f"{f'({slot_key})' if slot_key else ''}:目标库位已有 " + f"{type(current_resource).__name__}" + f"(value={current_repr}, len={current_len})" + f"(name={getattr(current_resource, 'name', None)}, " + f"parent={getattr(parent, 'name', None)}, " + f"uuid={getattr(current_resource, 'unilabos_uuid', None)})" + ) else: logger.warning(f"❌ 物料 {unique_name} 的索引 {idx} 超出仓库 {wh_name} 容量 {warehouse.capacity}") else: