diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py index 333b7b28e..c03bf7763 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from cgi import print_arguments from doctest import debug -from typing import Dict, Any, List, Optional +from typing import Dict, Any, List, Optional, Tuple, Union import requests from pylabrobot.resources.resource import Resource as ResourcePLR from pathlib import Path @@ -17,7 +17,7 @@ # ⚠️ config.py 已废弃 - 所有配置现在从 JSON 文件加载 # from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, ... from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService -from unilabos.resources.bioyond.decks import BIOYOND_YB_Deck +from unilabos.resources.bioyond.decks import BioyondElectrolyteDeck, bioyond_electrolyte_deck from unilabos.utils.log import logger from unilabos.registry.registry import lab_registry @@ -614,9 +614,12 @@ def auto_feeding4to3( response = self._post_lims("/api/lims/order/auto-feeding4to3", items) # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") + if response is None: + logger.error("上料 API 返回了空响应(None),服务端可能因入参问题返回了 null body,请检查物料条目是否合法。") + return {"code": -1, "message": "API returned None response"} + order_code = (response.get("data") or {}).get("orderCode") if not order_code: - logger.error("上料任务未返回有效 orderCode!") + logger.error(f"上料任务未返回有效 orderCode!完整响应:{response}") return response # 等待完成报送 result = self.wait_for_order_finish(order_code) @@ -694,226 +697,138 @@ def as_str(v, d=""): self.wait_for_response_orders(response, "auto_batch_outbound_from_xlsx") return response - # 2.14 新建实验 - def create_orders(self, xlsx_path: str) -> Dict[str, Any]: + # -------------------- 订单提交/等待/后处理(公共逻辑) -------------------- + def _submit_and_wait_orders(self, orders: List[Dict[str, Any]], tag: str = "create_orders") -> Dict[str, Any]: """ - 从 Excel 解析并创建实验(2.14) - 约定: - - batchId = Excel 文件名(不含扩展名) - - 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列) - - totalMass 自动计算为所有物料质量之和 - - createTime 缺失或为空时自动填充为当前日期(YYYY/M/D) + 公共流程:提交 orders → 等待完成 → 计算质量比 → 提取分液瓶板 → 返回结果。 + 由 create_orders / create_orders_formulation 调用。 """ - default_path = Path("D:\\UniLab\\Uni-Lab-OS\\unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\2025122301.xlsx") - path = Path(xlsx_path) if xlsx_path else default_path - print(f"[create_orders] 使用 Excel 路径: {path}") - if path != default_path: - print("[create_orders] 来源: 调用方传入自定义路径") - else: - print("[create_orders] 来源: 使用默认模板路径") - - if not path.exists(): - print(f"[create_orders] ⚠️ Excel 文件不存在: {path}") - raise FileNotFoundError(f"未找到 Excel 文件:{path}") - - try: - df = pd.read_excel(path, sheet_name=0, engine="openpyxl") - except Exception as e: - raise RuntimeError(f"读取 Excel 失败:{e}") - print(f"[create_orders] Excel 读取成功,行数: {len(df)}, 列: {list(df.columns)}") - - # 列名容错:返回可选列名,找不到则返回 None - def _pick(col_names: List[str]) -> Optional[str]: - for c in col_names: - if c in df.columns: - return c - return None - - col_order_name = _pick(["配方ID", "orderName", "订单编号"]) - col_create_time = _pick(["创建日期", "createTime"]) - col_bottle_type = _pick(["配液瓶类型", "bottleType"]) - col_mix_time = _pick(["混匀时间(s)", "mixTime"]) - col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"]) - col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"]) - col_cond = _pick(["电导测试分液体积", "conductivityInfo"]) - col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"]) - print("[create_orders] 列匹配结果:", { - "order_name": col_order_name, - "create_time": col_create_time, - "bottle_type": col_bottle_type, - "mix_time": col_mix_time, - "load": col_load, - "pouch": col_pouch, - "conductivity": col_cond, - "conductivity_bottle_count": col_cond_cnt, - }) - - # 物料列:所有以 (g) 结尾 - material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")] - print(f"[create_orders] 识别到的物料列: {material_cols}") - if not material_cols: - raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。") - - batch_id = path.stem - - def _to_ymd_slash(v) -> str: - # 统一为 "YYYY/M/D";为空或解析失败则用当前日期 - if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "": - ts = datetime.now() - else: - try: - ts = pd.to_datetime(v) - except Exception: - ts = datetime.now() - return f"{ts.year}/{ts.month}/{ts.day}" - - def _as_int(val, default=0) -> int: - try: - if pd.isna(val): - return default - return int(val) - except Exception: - return default - - def _as_float(val, default=0.0) -> float: - try: - if pd.isna(val): - return default - return float(val) - except Exception: - return default - - def _as_str(val, default="") -> str: - if val is None or (isinstance(val, float) and pd.isna(val)): - return default - s = str(val).strip() - return s if s else default - - orders: List[Dict[str, Any]] = [] - - for idx, row in df.iterrows(): - mats: List[Dict[str, Any]] = [] - total_mass = 0.0 - - for mcol in material_cols: - val = row.get(mcol, None) - if val is None or (isinstance(val, float) and pd.isna(val)): - continue - try: - mass = float(val) - except Exception: - continue - if mass > 0: - mats.append({"name": mcol.replace("(g)", ""), "mass": mass}) - total_mass += mass - else: - if mass < 0: - print(f"[create_orders] 第 {idx+1} 行物料 {mcol} 数值为负数: {mass}") - - order_data = { - "batchId": batch_id, - "orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}", - "createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None), - "bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶", - "mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0, - "loadSheddingInfo": _as_float(row[col_load]) if col_load else 0.0, - "pouchCellInfo": _as_float(row[col_pouch]) if col_pouch else 0, - "conductivityInfo": _as_float(row[col_cond]) if col_cond else 0, - "conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0, - "materialInfos": mats, - "totalMass": round(total_mass, 4) # 自动汇总 - } - print(f"[create_orders] 第 {idx+1} 行解析结果: orderName={order_data['orderName']}, " - f"loadShedding={order_data['loadSheddingInfo']}, pouchCell={order_data['pouchCellInfo']}, " - f"conductivity={order_data['conductivityInfo']}, totalMass={order_data['totalMass']}, " - f"material_count={len(mats)}") - - if order_data["totalMass"] <= 0: - print(f"[create_orders] ⚠️ 第 {idx+1} 行总质量 <= 0,可能导致 LIMS 校验失败") - if not mats: - print(f"[create_orders] ⚠️ 第 {idx+1} 行未找到有效物料") - - orders.append(order_data) - print("================================================") - print("orders:", orders) - - print(f"[create_orders] 即将提交订单数量: {len(orders)}") + logger.info(f"[{tag}] 即将提交 {len(orders)} 个订单") response = self._post_lims("/api/lims/order/orders", orders) - print(f"[create_orders] 接口返回: {response}") - - # 提取所有返回的 orderCode + logger.info(f"[{tag}] 接口返回: {response}") + + # 提取 orderCode data_list = response.get("data", []) if not data_list: logger.error("创建订单未返回有效数据!") return response - - # 收集所有 orderCode - order_codes = [] - for order_item in data_list: - code = order_item.get("orderCode") - if code: - order_codes.append(code) - + + order_codes = [item.get("orderCode") for item in data_list if item.get("orderCode")] if not order_codes: logger.error("未找到任何有效的 orderCode!") return response - - print(f"[create_orders] 等待 {len(order_codes)} 个订单完成: {order_codes}") - - # 等待所有订单完成并收集报文 + + logger.info(f"[{tag}] 等待 {len(order_codes)} 个订单完成: {order_codes}") + + # ========== 等待所有订单完成 ========== all_reports = [] for idx, order_code in enumerate(order_codes, 1): - print(f"[create_orders] 正在等待第 {idx}/{len(order_codes)} 个订单: {order_code}") + logger.info(f"[{tag}] 等待第 {idx}/{len(order_codes)} 个订单: {order_code}") result = self.wait_for_order_finish(order_code) - - # 提取报文数据 if result.get("status") == "success": - report = result.get("report", {}) - - # [新增] 处理试剂数据,计算质量比 + all_reports.append(result.get("report", {})) + logger.info(f"[{tag}] ✓ 订单 {order_code} 完成") + else: + logger.warning(f"订单 {order_code} 状态异常: {result.get('status')}") + all_reports.append({ + "orderCode": order_code, + "status": result.get("status"), + "error": result.get("message", "未知错误"), + }) + + logger.info(f"[{tag}] 所有订单已完成,共收集 {len(all_reports)} 个报文") + + # ========== 计算质量比 ========== + all_mass_ratios = [] + for idx, report in enumerate(all_reports, 1): + order_code = report.get("orderCode", "N/A") + if "error" not in report: try: mass_ratios = self._process_order_reagents(report) - report["mass_ratios"] = mass_ratios # 添加到报文中 - logger.info(f"已计算订单 {order_code} 的试剂质量比") + all_mass_ratios.append({ + "orderCode": order_code, + "orderName": report.get("orderName", "N/A"), + "real_mass_ratio": mass_ratios.get("real_mass_ratio", {}), + "target_mass_ratio": mass_ratios.get("target_mass_ratio", {}), + }) + logger.info(f"✓ 已计算订单 {order_code} 的试剂质量比") except Exception as e: - logger.error(f"计算试剂质量比失败: {e}") - report["mass_ratios"] = { + logger.error(f"计算订单 {order_code} 质量比失败: {e}") + all_mass_ratios.append({ + "orderCode": order_code, + "orderName": report.get("orderName", "N/A"), "real_mass_ratio": {}, "target_mass_ratio": {}, - "reagent_details": [], - "error": str(e) - } - - all_reports.append(report) - print(f"[create_orders] ✓ 订单 {order_code} 完成") + "error": str(e), + }) else: - logger.warning(f"订单 {order_code} 状态异常: {result.get('status')}") - # 即使订单失败,也记录下这个结果 - all_reports.append({ + all_mass_ratios.append({ "orderCode": order_code, - "status": result.get("status"), - "error": result.get("message", "未知错误") + "orderName": report.get("orderName", "N/A"), + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "error": "订单未成功完成", }) - - print(f"[create_orders] 所有订单已完成,共收集 {len(all_reports)} 个报文") - print("实验记录本========================create_orders========================") - - # 返回所有订单的完成报文 + + logger.info(f"[{tag}] 质量比计算完成") + + # ========== 提取分液瓶板信息 + 创建资源树对象 ========== + all_vial_plates = [] + processed_material_ids = set() + for report in all_reports: + vial_plate_info = self._extract_vial_plate_from_report(report) + if vial_plate_info: + material_id = vial_plate_info.get("materialId") + all_vial_plates.append(vial_plate_info) + if material_id in processed_material_ids: + logger.info( + f"[资源树] ℹ️ 瓶板资源已存在: materialId={material_id[:20]}..., " + f"orderCode={vial_plate_info.get('orderCode')} (共用同一瓶板,跳过重复创建)" + ) + continue + try: + self._create_vial_plate_resource(vial_plate_info) + processed_material_ids.add(material_id) + logger.info( + f"[资源树] ✅ 瓶板资源创建成功: orderCode={vial_plate_info.get('orderCode')}, " + f"materialId={material_id[:20]}..." + ) + except Exception as e: + logger.error( + f"[资源树] 创建失败: orderCode={vial_plate_info.get('orderCode')}, 错误={e}" + ) + + logger.info( + f"[{tag}] 提取到 {len(all_vial_plates)} 个订单的分液瓶板信息 " + f"(对应 {len(processed_material_ids)} 个物理瓶板)" + ) + + # ========== 构造最终结果 ========== final_result = { "status": "all_completed", "total_orders": len(order_codes), + "bottle_count": len(order_codes), "reports": all_reports, - "original_response": response + "mass_ratios": all_mass_ratios, + "vial_plates": all_vial_plates, + "original_response": response, } - - print(f"返回报文数量: {len(all_reports)}") - for i, report in enumerate(all_reports, 1): - print(f"报文 {i}: orderCode={report.get('orderCode', 'N/A')}, status={report.get('status', 'N/A')}") - print("========================") - + + logger.info("=" * 80) + logger.info(f"[{tag}] 返回报文数量: {len(all_reports)}, 分液瓶板数量: {len(all_vial_plates)}") + for idx, vial_plate in enumerate(all_vial_plates, 1): + logger.info( + f" [{idx}] orderCode={vial_plate.get('orderCode', 'N/A')}, " + f"materialId={vial_plate.get('materialId', 'N/A')[:20]}..., " + f"locationId={vial_plate.get('locationId', 'N/A')[:20]}..., " + f"typeName={vial_plate.get('typeName', 'N/A')}" + ) + logger.info("=" * 80) + return final_result - def create_orders_v2(self, xlsx_path: str) -> Dict[str, Any]: + # -------------------- 2.14 新建实验(Excel 入口) -------------------- + def create_orders(self, xlsx_path: str) -> Dict[str, Any]: """ 从 Excel 解析并创建实验(2.14)- V2版本 约定: @@ -1052,112 +967,805 @@ def _as_str(val, default="") -> str: print(f"[create_orders_v2] ⚠️ 第 {idx+1} 行未找到有效物料") orders.append(order_data) - print("================================================") - print("orders:", orders) - print(f"[create_orders_v2] 即将提交订单数量: {len(orders)}") - response = self._post_lims("/api/lims/order/orders", orders) - print(f"[create_orders_v2] 接口返回: {response}") + if not orders: + logger.error("[create_orders] 没有有效的订单可提交") + return {"status": "error", "message": "没有有效订单数据"} + + return self._submit_and_wait_orders(orders, tag="create_orders") + + def create_orders_formulation( + self, + formulation: List[Dict[str, Any]], + batch_id: str = "", + bottle_type: str = "配液小瓶", + mix_time: int = 0, + load_shedding_info: float = 0.0, + pouch_cell_info: float = 0.0, + conductivity_info: float = 0.0, + conductivity_bottle_count: int = 0, + ) -> Dict[str, Any]: + """ + 配方批量输入版本的 create_orders —— 等价于 create_orders, + 但参数来源于前端 FormulationBatchWidget,而非 Excel 文件。 + + Args: + formulation: 配方列表,每个元素代表一个订单(一瓶),格式: + [ + { + "order_name": "配方A", # 可选,配方名称 + "materials": [ # 物料列表 + {"name": "LiPF6", "mass": 12.5}, + {"name": "EC", "mass": 50.0}, + ] + }, + ... + ] + batch_id: 批次ID,若为空则用当前时间戳 + bottle_type: 配液瓶类型,默认 "配液小瓶" + mix_time: 混匀时间(秒) + load_shedding_info: 扣电组装分液体积 + pouch_cell_info: 软包组装分液体积 + conductivity_info: 电导测试分液体积 + conductivity_bottle_count: 电导测试分液瓶数 + + Returns: + 与 create_orders 返回格式一致的结果字典 + """ + if not formulation: + raise ValueError("formulation 参数不能为空") + + if not batch_id: + batch_id = f"formulation_{datetime.now().strftime('%Y%m%d%H%M%S')}" + + create_time = f"{datetime.now().year}/{datetime.now().month}/{datetime.now().day}" + + # 将 formulation 转换为 LIMS orders 格式(与 create_orders 中的格式一致) + orders: List[Dict[str, Any]] = [] + for idx, item in enumerate(formulation): + materials = item.get("materials", []) + item.get("liquids", []) # 兼容两种物料列表命名 + order_name = item.get("order_name", f"{batch_id}_order_{idx + 1}") + + mats: List[Dict[str, Any]] = [] + total_mass = 0.0 + for mat in materials: + name = mat.get("name", "") + mass = float(mat.get("mass", mat.get("volume", 0.0))) + if name and mass > 0: + mats.append({"name": name, "mass": mass}) + total_mass += mass + + if not mats: + logger.warning(f"[create_orders_formulation] 第 {idx + 1} 个配方无有效物料,跳过") + continue + + logger.info(f"[create_orders_formulation] 第 {idx + 1} 个配方: orderName={order_name}, " + f"loadShedding={load_shedding_info}, pouchCell={pouch_cell_info}, " + f"conductivity={conductivity_info}, totalMass={total_mass}, " + f"material_count={len(mats)}") + + orders.append({ + "batchId": batch_id, + "orderName": order_name, + "createTime": create_time, + "bottleType": bottle_type, + "mixTime": mix_time, + "loadSheddingInfo": load_shedding_info, + "pouchCellInfo": pouch_cell_info, + "conductivityInfo": conductivity_info, + "conductivityBottleCount": conductivity_bottle_count, + "materialInfos": mats, + "totalMass": round(total_mass, 4), + }) + + if not orders: + logger.error("[create_orders_formulation] 没有有效的订单可提交") + return {"status": "error", "message": "没有有效配方数据"} + + return self._submit_and_wait_orders(orders, tag="create_orders_formulation") + + def _extract_vial_plate_from_report(self, report: Dict) -> Optional[Dict]: + """ + 从 order_finish 报文中提取分液瓶板信息 + + Args: + report: LIMS order_finish 报文 + + Returns: + { + "materialId": "...", + "locationId": "...", + "orderCode": "...", + "typeName": "5ml分液瓶板", # 可选 + "barCode": "..." # 可选 + } + """ + order_code = report.get("orderCode", "N/A") + used_materials = report.get("usedMaterials", []) + + # ========== 新增:调试日志 ========== + logger.info( + f"[提取分液瓶板] 开始处理订单 orderCode={order_code}, " + f"物料数量={len(used_materials)}" + ) + + # 配置:自动堆栈-左的 locationId 前缀 + AUTO_STACK_LEFT_PREFIX = "3a19debc-84b5-" + + for idx, material in enumerate(used_materials): + location_id = material.get("locationId", "") + typemode = material.get("typemode", "") + material_id = material.get("materialId", "") + + logger.debug( + f"[提取分液瓶板] 物料 #{idx+1}: materialId={material_id[:20]}..., " + f"locationId={location_id[:20] if location_id else 'None'}..., " + f"typemode={typemode}" + ) + + # 判断条件:typemode=1 且 locationId 以自动堆栈-左前缀开头 + # ⚠️ 检查 location_id 不为 None + if typemode == "1" and location_id and location_id.startswith(AUTO_STACK_LEFT_PREFIX): + logger.info( + f"[提取分液瓶板] 找到候选物料: materialId={material_id}, " + f"locationId={location_id}" + ) + + # 可选:调用 LIMS API 2.4 获取详细信息 + try: + material_info = self._query_material_info(material_id) + type_name = material_info.get("typeName", "") + + # 确认是分液瓶板 + if "分液瓶板" in type_name: + logger.info( + f"[提取分液瓶板] ✅ 确认为分液瓶板: orderCode={order_code}, " + f"materialId={material_id}, locationId={location_id}, " + f"typeName={type_name}" + ) + return { + "materialId": material_id, + "locationId": location_id, + "orderCode": order_code, + "typeName": type_name, + "barCode": material_info.get("barCode") + } + else: + logger.warning( + f"[提取分液瓶板] ⚠️ 候选物料不是分液瓶板: typeName={type_name}, " + f"跳过并继续搜索" + ) + except Exception as e: + logger.warning( + f"[提取分液瓶板] ⚠️ 查询物料详情失败: materialId={material_id}, " + f"错误={str(e)}, 返回基本信息" + ) + # 即使查询失败,也返回基本信息 + return { + "materialId": material_id, + "locationId": location_id, + "orderCode": order_code + } + + logger.warning(f"[提取分液瓶板] ❌ 未找到分液瓶板: orderCode={order_code}") + return None + + def _query_material_info(self, material_id: str) -> Dict: + """ + 调用 LIMS API 2.4 查询物料详情 + + Args: + material_id: 物料ID (materialId) + + Returns: + { + "typeName": "5ml分液瓶板", + "barCode": "...", + "name": "...", + "detail": [...] + } + """ + # 从配置加载 api_key和api_host(用于日志) + api_key = self.bioyond_config.get("api_key", "8A819E5C") + api_host = self.bioyond_config.get("api_host", "UNKNOWN") + + # ========== 调试日志 ========== + logger.info( + f"[查询物料详情] 开始查询 materialId={material_id}, " + f"api_host={api_host}, api_key={api_key[:4]}****" + ) + + try: + # 直接传递 material_id,_post_lims 会自动包装为 {apiKey, requestTime, data} + response = self._post_lims("/api/lims/storage/material-info", material_id) + + logger.debug(f"[查询物料详情] API响应: code={response.get('code')}, message={response.get('message')}") + + if response.get("code") == 1: + data = response.get("data", {}) + logger.info( + f"[查询物料详情] ✅ 成功: materialId={material_id}, " + f"typeName={data.get('typeName')}, barCode={data.get('barCode')}" + ) + return data + else: + error_msg = f"查询物料详情失败: {response.get('message')}" + logger.warning(f"[查询物料详情] ❌ {error_msg}") + raise ValueError(error_msg) + except Exception as e: + logger.error( + f"[查询物料详情] ❌ 异常: materialId={material_id}, " + f"错误类型={type(e).__name__}, 错误信息={str(e)}" + ) + raise + + def _create_vial_plate_resource(self, vial_plate_info: Dict) -> None: + """ + 创建分液瓶板资源对象并添加到资源树 + + Args: + vial_plate_info: 分液瓶板元数据 + { + "materialId": "3a1f3df9-ddce-f544-bd48-07077ad87bc5", + "locationId": "3a19debc-84b5-4c1c-d3a1-26830cf273ff", + "orderCode": "BSO2026020500002", + "typeName": "5ml分液瓶板" 或 "20ml分液瓶板" + } + """ + from unilabos.resources.bioyond.YB_bottle_carriers import ( + YB_Vial_5mL_Carrier, + YB_Vial_20mL_Carrier + ) + + material_id = vial_plate_info["materialId"] + location_id = vial_plate_info["locationId"] + order_code = vial_plate_info["orderCode"] + type_name = vial_plate_info["typeName"] + + logger.info( + f"[资源树] 开始创建分液瓶板: orderCode={order_code}, " + f"typeName={type_name}" + ) + + # 1. 根据类型创建Carrier对象 + if "5ml" in type_name.lower() or "5mL" in type_name: + vial_plate_obj = YB_Vial_5mL_Carrier( + name=f"vial_plate_{order_code}" + ) + logger.debug(f"[资源树] 创建 YB_Vial_5mL_Carrier: {vial_plate_obj.name}") + elif "20ml" in type_name.lower() or "20mL" in type_name: + vial_plate_obj = YB_Vial_20mL_Carrier( + name=f"vial_plate_{order_code}" + ) + logger.debug(f"[资源树] 创建 YB_Vial_20mL_Carrier: {vial_plate_obj.name}") + else: + logger.warning( + f"[资源树] ⚠️ 未知的分液瓶板类型: {type_name}, 跳过创建" + ) + return + + # ✅ 关键:分配 UUID(用于资源树转运) + # 使用 materialId 作为 UUID,确保与LIMS系统一致 + vial_plate_obj.unilabos_uuid = material_id + logger.debug(f"[资源树] 分配 UUID: {material_id[:30]}...") + + # ✅ 新增:查询并创建分液瓶板上的瓶子资源 + try: + self._populate_vial_bottles(vial_plate_obj, material_id, order_code) + except Exception as e: + logger.warning( + f"[资源树] ⚠️ 创建瓶子资源失败(继续创建瓶板): {e}" + ) + + # 2. 解析位置 (locationId → warehouse + slot) + wh_name, slot_name = self._get_warehouse_and_slot_from_location_id( + location_id + ) + + if not wh_name or not slot_name: + logger.warning( + f"[资源树] ⚠️ 无法解析位置: locationId={location_id}, " + f"wh_name={wh_name}, slot_name={slot_name}" + ) + return + + logger.debug( + f"[资源树] 解析位置: locationId={location_id[:20]}... → " + f"{wh_name}[{slot_name}]" + ) + + # 3. 添加到资源树 + try: + warehouse = self.deck.get_resource(wh_name) + if not warehouse: + logger.error(f"[资源树] ❌ 未找到仓库: {wh_name}") + return + + # 使用直接槽位赋值 + # warehouse 的 sites 是一个 dict: {"A01": ResourceHolder, "A02": ...} + # 直接通过 warehouse[slot_name] 访问槽位并赋值资源对象 + warehouse[slot_name] = vial_plate_obj + + logger.info( + f"[资源树] ✅ 创建成功: {wh_name}[{slot_name}] = " + f"{vial_plate_obj.name} (类型: {type_name})" + ) + except Exception as e: + logger.error( + f"[资源树] ❌ 添加到资源树失败: {wh_name}[{slot_name}], " + f"错误={e}" + ) + raise + + def _populate_vial_bottles( + self, + vial_plate_obj, + plate_material_id: str, + order_code: str + ) -> None: + """ + 查询分液瓶板的detail信息,创建瓶子资源并添加到瓶板 + + Args: + vial_plate_obj: 瓶板资源对象 + plate_material_id: 瓶板的materialId + order_code: 订单号 + """ + logger.info(f"[资源树] 查询瓶板子物料: materialId={plate_material_id[:20]}...") - # 提取所有返回的 orderCode - data_list = response.get("data", []) - if not data_list: - logger.error("创建订单未返回有效数据!") - return response + # 1. 调用LIMS接口查询瓶板详情 + try: + plate_detail = self.get_material_info(plate_material_id) + except Exception as e: + logger.error(f"[资源树] ❌ 查询瓶板详情失败: {e}") + return - # 收集所有 orderCode - order_codes = [] - for order_item in data_list: - code = order_item.get("orderCode") - if code: - order_codes.append(code) + # 2. 提取detail字段(包含所有瓶子信息) + bottles_detail = plate_detail.get("detail", []) + if not bottles_detail: + logger.warning(f"[资源树] ⚠️ 瓶板无子物料信息") + return - if not order_codes: - logger.error("未找到任何有效的 orderCode!") - return response + logger.info(f"[资源树] 瓶板包含 {len(bottles_detail)} 个瓶子") - print(f"[create_orders_v2] 等待 {len(order_codes)} 个订单完成: {order_codes}") + # 3. 为每个瓶子创建资源 + from unilabos.resources.bioyond.YB_bottles import YB_Vial_5mL - # ========== 步骤1: 等待所有订单完成并收集报文(不计算质量比)========== - all_reports = [] - for idx, order_code in enumerate(order_codes, 1): - print(f"[create_orders_v2] 正在等待第 {idx}/{len(order_codes)} 个订单: {order_code}") - result = self.wait_for_order_finish(order_code) - - # 提取报文数据 - if result.get("status") == "success": - report = result.get("report", {}) - all_reports.append(report) - print(f"[create_orders_v2] ✓ 订单 {order_code} 完成") - else: - logger.warning(f"订单 {order_code} 状态异常: {result.get('status')}") - # 即使订单失败,也记录下这个结果 - all_reports.append({ + created_count = 0 + for idx, bottle_info in enumerate(bottles_detail, 1): + try: + bottle_material_id = bottle_info.get("detailMaterialId") + bottle_code = bottle_info.get("code", f"bottle_{idx}") + bottle_x = bottle_info.get("x", 0) + bottle_y = bottle_info.get("y", 0) + associate_id = bottle_info.get("associateId") # 关联订单ID + + if not bottle_material_id: + logger.warning(f" 瓶子[{idx}]: 缺少materialId,跳过") + continue + + # ✅ 创建瓶子资源(使用工厂函数) + bottle_obj = YB_Vial_5mL( + name=f"{vial_plate_obj.name}_vial_{bottle_code.replace(' ', '_')}", + diameter=20.0, + height=50.0, + max_volume=5000.0, # 5mL + barcode=None + ) + + # ✅ 设置UUID(用于LIMS同步) + bottle_obj.unilabos_uuid = bottle_material_id + + # ✅ 存储元数据(供扣电使用) + bottle_obj._unilabos_state = { "orderCode": order_code, - "status": result.get("status"), - "error": result.get("message", "未知错误") - }) + "materialId": bottle_material_id, + "code": bottle_code, + "position_x": bottle_x, + "position_y": bottle_y, + "associateId": associate_id + } + + # ✅ 添加到瓶板(根据xy坐标计算索引) + # 假设瓶板布局: x=1,2 y=1,2,3,4 (2x4布局) + bottle_index = (bottle_x - 1) * 4 + (bottle_y - 1) + + if 0 <= bottle_index < len(vial_plate_obj.children): + vial_plate_obj.children[bottle_index] = bottle_obj + created_count += 1 + logger.debug( + f" 瓶子[{idx}]: code={bottle_code}, " + f"位置=({bottle_x},{bottle_y}), 索引={bottle_index}" + ) + else: + logger.warning( + f" 瓶子[{idx}]: 索引超出范围 ({bottle_index} >= {len(vial_plate_obj.children)})" + ) + + except Exception as e: + logger.warning(f" 瓶子[{idx}]: 创建失败 - {e}") + continue + + logger.info(f"[资源树] ✅ 已创建 {created_count}/{len(bottles_detail)} 个瓶子资源") + + def transfer_3_to_2_to_1_auto( + self, + vial_plates: List[Dict], + target_device: str = "BatteryStation", + target_location: str = "bottle_rack_6x2", + mass_ratios: List[Dict] = None, # ✅ 新增:配方信息(用于瓶子放置位置映射) + **kwargs # 兼容性参数,捕获已废弃的 vial_plate_info 等参数 + ) -> Dict[str, Any]: + """ + 自动转运(从 create_orders 结果自动定位源位置) - print(f"[create_orders_v2] 所有订单已完成,共收集 {len(all_reports)} 个报文") + Args: + vial_plates: 分液瓶板列表 + 格式: [{"materialId": "...", "locationId": "...", "orderCode": "..."}, ...] + target_device: 目标设备ID + target_location: 目标资源名称 + mass_ratios: 配方信息列表(可选),用于确定瓶子在bottle_rack的位置 + 格式: [{"orderCode": "...", "real_mass_ratio": {...}, ...}, ...] + **kwargs: 兼容性参数,用于捕获已废弃的参数(如 vial_plate_info) - # ========== 步骤2: 统一计算所有订单的质量比 ========== - print(f"[create_orders_v2] 开始统一计算 {len(all_reports)} 个订单的质量比...") - all_mass_ratios = [] # 存储所有订单的质量比,与reports顺序一致 + Returns: + { + "total": 转运总数, + "success": 成功数量, + "failed": 失败数量, + "results": [每个转运的详细结果] + } + """ + # 检查是否传递了已废弃的参数 + if kwargs: + logger.warning( + f"[transfer_3_to_2_to_1_auto] ⚠️ 检测到已废弃的参数: {list(kwargs.keys())}, " + f"这些参数将被忽略" + ) - for idx, report in enumerate(all_reports, 1): - order_code = report.get("orderCode", "N/A") - print(f"[create_orders_v2] 计算第 {idx}/{len(all_reports)} 个订单 {order_code} 的质量比...") - - # 只为成功完成的订单计算质量比 - if "error" not in report: - try: - mass_ratios = self._process_order_reagents(report) - # 精简输出,只保留核心质量比信息 - all_mass_ratios.append({ - "orderCode": order_code, - "orderName": report.get("orderName", "N/A"), - "real_mass_ratio": mass_ratios.get("real_mass_ratio", {}), - "target_mass_ratio": mass_ratios.get("target_mass_ratio", {}) + # ========== 参数验证 ========== + if not vial_plates: + raise ValueError("vial_plates 参数不能为空") + + logger.info("=" * 80) + logger.info(f"[transfer_3_to_2_to_1_auto] 接收到 {len(vial_plates)} 个分液瓶板") + for idx, plate in enumerate(vial_plates, 1): + logger.info( + f" [{idx}] orderCode={plate.get('orderCode', 'N/A')}, " + f"materialId={plate.get('materialId', 'N/A')[:20]}..." + ) + logger.info("=" * 80) + + # ========== 步骤2:依次转运每个分液瓶板(去重,同一瓶板只转运一次)========== + results = [] + success_count = 0 + failed_count = 0 + transferred_material_ids = set() # ✅ 记录已转运的materialId + + logger.info( + f"[批量转运] 开始转运 {len(vial_plates)} 个订单的分液瓶板 → " + f"{target_device}.{target_location}" + ) + + for idx, plate_info in enumerate(vial_plates, 1): + try: + # ✅ 检查 plate_info 是否有效 + if not plate_info or not isinstance(plate_info, dict): + logger.error( + f"[批量转运] ❌ [{idx}/{len(vial_plates)}] 分液瓶板信息无效: {plate_info}" + ) + results.append({ + "index": idx, + "orderCode": "N/A", + "materialId": "N/A", + "status": "failed", + "error": "分液瓶板信息无效或为空" }) - logger.info(f"✓ 已计算订单 {order_code} 的试剂质量比") - except Exception as e: - logger.error(f"计算订单 {order_code} 质量比失败: {e}") - all_mass_ratios.append({ + failed_count += 1 + continue + + material_id = plate_info.get('materialId') + order_code = plate_info.get('orderCode', 'N/A') + + logger.info(f"\n{'='*60}") + logger.info(f"[批量转运] 处理 [{idx}/{len(vial_plates)}]") + logger.info(f" orderCode: {order_code}") + logger.info(f" materialId: {material_id[:20] if material_id else 'N/A'}...") + + # ✅ 检查是否已转运(同一物理瓶板只转运一次) + if material_id in transferred_material_ids: + logger.info( + f" ℹ️ 该瓶板已转运,跳过 (多订单共用同一瓶板)" + ) + results.append({ + "index": idx, "orderCode": order_code, - "orderName": report.get("orderName", "N/A"), - "real_mass_ratio": {}, - "target_mass_ratio": {}, - "error": str(e) + "materialId": material_id, + "status": "skipped", + "message": "该瓶板已转运(共用瓶板)" }) - else: - # 失败的订单不计算质量比 - all_mass_ratios.append({ + success_count += 1 # 视为成功 + logger.info(f"{'='*60}") + continue + + logger.info(f"{'='*60}") + + # 调用单个转运逻辑 + result = self._transfer_single_vial_plate( + vial_plate_info=plate_info, + target_device=target_device, + target_location=target_location + ) + + transferred_material_ids.add(material_id) + results.append({ + "index": idx, "orderCode": order_code, - "orderName": report.get("orderName", "N/A"), - "real_mass_ratio": {}, - "target_mass_ratio": {}, - "error": "订单未成功完成" + "materialId": material_id, + "status": "success", + "result": result }) + success_count += 1 + logger.info(f"[批量转运] ✅ [{idx}/{len(vial_plates)}] 转运成功") + + except Exception as e: + logger.error( + f"[批量转运] ❌ [{idx}/{len(vial_plates)}] 失败: {str(e)}" + ) + results.append({ + "index": idx, + "orderCode": plate_info.get("orderCode", "N/A") if plate_info else "N/A", + "materialId": plate_info.get("materialId", "N/A") if plate_info else "N/A", + "status": "failed", + "error": str(e) + }) + failed_count += 1 + + # ========== 步骤3:汇总结果 ========== + summary = { + "total": len(vial_plates), + "success": success_count, + "failed": failed_count, + "results": results + } - print(f"[create_orders_v2] 质量比计算完成") - print("实验记录本========================create_orders_v2========================") + logger.info(f"\n{'='*60}") + logger.info(f"[批量转运] 完成汇总:") + logger.info(f" 总数: {summary['total']}") + logger.info(f" 成功: {summary['success']} ✅") + logger.info(f" 失败: {summary['failed']} ❌") + logger.info(f"{'='*60}\n") - # 返回所有订单的完成报文 - final_result = { - "status": "all_completed", - "total_orders": len(order_codes), - "bottle_count": len(order_codes), # 明确标注瓶数,用于下游check - "reports": all_reports, # 原始订单报文(不含质量比) - "mass_ratios": all_mass_ratios, # 所有质量比统一放在这里 - "original_response": response - } + return summary + + def _transfer_single_vial_plate( + self, + vial_plate_info: Dict, + target_device: str, + target_location: str + ) -> Dict[str, Any]: + """ + 转运单个分液瓶板(内部方法) - print(f"返回报文数量: {len(all_reports)}") - for i, report in enumerate(all_reports, 1): - print(f"报文 {i}: orderCode={report.get('orderCode', 'N/A')}, status={report.get('status', 'N/A')}") - print("========================") + Args: + vial_plate_info: 单个分液瓶板信息 + target_device: 目标设备ID + target_location: 目标资源名称 - return final_result + Returns: + LIMS转运结果 + """ + location_id = vial_plate_info["locationId"] + material_id = vial_plate_info["materialId"] + + # 步骤1:locationId → warehouse名称 + 槽位名称 + wh_name, slot_name = self._get_warehouse_and_slot_from_location_id(location_id) + + if not wh_name or not slot_name: + raise ValueError(f"无法从 locationId 解析仓库和槽位: {location_id}") + + logger.info( + f"[自动转运] 分液瓶板位置: {wh_name}[{slot_name}], " + f"materialId={material_id}" + ) + + # 步骤2:获取 warehouse_id + warehouse_id = self._get_warehouse_id(wh_name) + + # 步骤3:槽位名称 → 坐标 + x, y, z = self._slot_to_coordinates(slot_name) + logger.info(f"[自动转运] 坐标: ({x}, {y}, {z})") + + # 步骤4:调用物理转运 + lims_result = self.transfer_3_to_2_to_1( + source_wh_id=warehouse_id, + source_x=x, + source_y=y, + source_z=z + ) + logger.info(f"[LIMS转运] 完成: {lims_result}") + + # 步骤5:资源树数字转运 + try: + # 获取 warehouse 对象 + warehouse = self.deck.get_resource(wh_name) + if not warehouse: + raise ValueError(f"资源树中未找到仓库: {wh_name}") + + # 通过槽位名称直接访问 + vial_plate = warehouse[slot_name] + + if vial_plate: + # ========== 获取目标资源对象 ========== + logger.info( + f"[资源同步] 准备目标资源: {target_device}.{target_location}" + ) + + # 从目标设备的资源树中获取真实的接驳槽对象(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 槽位," + f"且目标节点已启动并完成资源树初始化。" + ) + + logger.info( + f"[资源同步] 找到目标资源: {target_resource_obj.name}, " + f"UUID={getattr(target_resource_obj, 'unilabos_uuid', 'N/A')}" + ) + + # 执行资源树转移 + self.transfer_resource_to_another( + resource=[vial_plate], + mount_resource=[target_resource_obj], + sites=["electrolyte_buffer"], + mount_device_id=f"/devices/{target_device}" + ) + logger.info( + f"[资源同步] ✅ 成功: {vial_plate.name} → " + f"{target_device}.{target_location}" + ) + else: + logger.warning( + f"[资源同步] ⚠️ 警告: {wh_name}[{slot_name}] 槽位为空, " + f"可能资源树未及时更新" + ) + except Exception as e: + logger.error(f"[资源同步] ❌ 失败: {e}") + # 不中断流程,物理转运已完成 + + return lims_result + + def _get_resource_from_device( + self, + device_id: str, + resource_name: str, + ): + """ + 从指定设备的本地资源树中按名称查找 PLR 资源对象。 + + Args: + device_id: 目标设备 ID(如 "BatteryStation") + resource_name: 资源名称(如 "electrolyte_buffer") + + Returns: + 找到的 PLR Resource 对象,未找到则返回 None + """ + try: + from unilabos.app.ros2_app import get_device_plr_resource_by_name + return get_device_plr_resource_by_name(device_id, resource_name) + except Exception: + pass + + # 降级:遍历 workstation 已注册的 plr_resources 列表 + try: + for res in getattr(self, "_plr_resources", []): + if res.name == resource_name: + return res + found = res.get_resource(resource_name) if hasattr(res, "get_resource") else None + if found is not None: + return found + except Exception: + pass + + return None + + def _get_warehouse_and_slot_from_location_id( + self, + location_id: str + ) -> Tuple[Optional[str], Optional[str]]: + """ + 从 locationId 解析仓库名称和槽位名称 + + Args: + location_id: site_uuid, 例如 "3a19debc-84b5-4c1c-d3a1-26830cf273ff" + + Returns: + (warehouse_name, slot_name) + 例如:("自动堆栈-左", "A01") + """ + warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) + + for wh_name, wh_data in warehouse_mapping.items(): + site_uuids = wh_data.get("site_uuids", {}) + for slot_name, site_uuid in site_uuids.items(): + if site_uuid == location_id: + return (wh_name, slot_name) + + logger.error(f"未找到 locationId: {location_id}") + return (None, None) + + def _get_warehouse_id(self, warehouse_name: str) -> str: + """ + 获取仓库的 warehouse_id (uuid) + + 带降级逻辑:如果配置缺失,使用默认值(自动堆栈-左) + + Args: + warehouse_name: 仓库名称,例如 "自动堆栈-左" + + Returns: + warehouse_id + """ + warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) + wh_data = warehouse_mapping.get(warehouse_name, {}) + warehouse_id = wh_data.get("uuid") + + if not warehouse_id: + # 降级:使用默认值 + default_uuid = "3a19debc-84b4-0359-e2d4-b3beea49348b" + logger.warning( + f"仓库 '{warehouse_name}' 的 uuid 未配置, " + f"使用默认值: {default_uuid}" + ) + warehouse_id = default_uuid + + return warehouse_id + + def _slot_to_coordinates(self, slot_name: str) -> Tuple[int, int, int]: + """ + 槽位名称 → LIMS坐标 + + Args: + slot_name: 槽位名称,例如 "A01", "B02", "E03" + + Returns: + (x, y, z) 坐标元组 + + 转换规则: + - 字母 → x (A=1, B=2, C=3...) + - 数字 → y (01=1, 02=2, 03=3...) + - z 固定为 1 + + Examples: + >>> _slot_to_coordinates("A01") + (1, 1, 1) + >>> _slot_to_coordinates("B02") + (2, 2, 1) + >>> _slot_to_coordinates("E03") + (5, 3, 1) + """ + if not slot_name or len(slot_name) < 2: + raise ValueError(f"Invalid slot name: {slot_name}") + + letter = slot_name[0].upper() # 'A', 'B', 'C'... + number_str = slot_name[1:] # '01', '02', '03'... + + # 字母 → x + x = ord(letter) - ord('A') + 1 + + # 数字 → y + y = int(number_str) + + # z 固定为 1 + z = 1 + + return (x, y, z) + # 2.7 启动调度 def scheduler_start(self) -> Dict[str, Any]: @@ -1326,160 +1934,6 @@ def scheduler_start_and_auto_feeding( } - def scheduler_start_and_auto_feeding_v2( - self, - # ★ Excel路径参数 - xlsx_path: Optional[str] = "D:\\UniLab\\Uni-Lab-OS\\unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\material_template.xlsx", - # ---------------- WH4 - 加样头面 (Z=1, 12个点位) ---------------- - WH4_x1_y1_z1_1_materialName: str = "", WH4_x1_y1_z1_1_quantity: float = 0.0, - WH4_x2_y1_z1_2_materialName: str = "", WH4_x2_y1_z1_2_quantity: float = 0.0, - WH4_x3_y1_z1_3_materialName: str = "", WH4_x3_y1_z1_3_quantity: float = 0.0, - WH4_x4_y1_z1_4_materialName: str = "", WH4_x4_y1_z1_4_quantity: float = 0.0, - WH4_x5_y1_z1_5_materialName: str = "", WH4_x5_y1_z1_5_quantity: float = 0.0, - WH4_x1_y2_z1_6_materialName: str = "", WH4_x1_y2_z1_6_quantity: float = 0.0, - WH4_x2_y2_z1_7_materialName: str = "", WH4_x2_y2_z1_7_quantity: float = 0.0, - WH4_x3_y2_z1_8_materialName: str = "", WH4_x3_y2_z1_8_quantity: float = 0.0, - WH4_x4_y2_z1_9_materialName: str = "", WH4_x4_y2_z1_9_quantity: float = 0.0, - WH4_x5_y2_z1_10_materialName: str = "", WH4_x5_y2_z1_10_quantity: float = 0.0, - WH4_x1_y3_z1_11_materialName: str = "", WH4_x1_y3_z1_11_quantity: float = 0.0, - WH4_x2_y3_z1_12_materialName: str = "", WH4_x2_y3_z1_12_quantity: float = 0.0, - - # ---------------- WH4 - 原液瓶面 (Z=2, 9个点位) ---------------- - WH4_x1_y1_z2_1_materialName: str = "", WH4_x1_y1_z2_1_quantity: float = 0.0, WH4_x1_y1_z2_1_materialType: str = "", WH4_x1_y1_z2_1_targetWH: str = "", - WH4_x2_y1_z2_2_materialName: str = "", WH4_x2_y1_z2_2_quantity: float = 0.0, WH4_x2_y1_z2_2_materialType: str = "", WH4_x2_y1_z2_2_targetWH: str = "", - WH4_x3_y1_z2_3_materialName: str = "", WH4_x3_y1_z2_3_quantity: float = 0.0, WH4_x3_y1_z2_3_materialType: str = "", WH4_x3_y1_z2_3_targetWH: str = "", - WH4_x1_y2_z2_4_materialName: str = "", WH4_x1_y2_z2_4_quantity: float = 0.0, WH4_x1_y2_z2_4_materialType: str = "", WH4_x1_y2_z2_4_targetWH: str = "", - WH4_x2_y2_z2_5_materialName: str = "", WH4_x2_y2_z2_5_quantity: float = 0.0, WH4_x2_y2_z2_5_materialType: str = "", WH4_x2_y2_z2_5_targetWH: str = "", - WH4_x3_y2_z2_6_materialName: str = "", WH4_x3_y2_z2_6_quantity: float = 0.0, WH4_x3_y2_z2_6_materialType: str = "", WH4_x3_y2_z2_6_targetWH: str = "", - WH4_x1_y3_z2_7_materialName: str = "", WH4_x1_y3_z2_7_quantity: float = 0.0, WH4_x1_y3_z2_7_materialType: str = "", WH4_x1_y3_z2_7_targetWH: str = "", - WH4_x2_y3_z2_8_materialName: str = "", WH4_x2_y3_z2_8_quantity: float = 0.0, WH4_x2_y3_z2_8_materialType: str = "", WH4_x2_y3_z2_8_targetWH: str = "", - WH4_x3_y3_z2_9_materialName: str = "", WH4_x3_y3_z2_9_quantity: float = 0.0, WH4_x3_y3_z2_9_materialType: str = "", WH4_x3_y3_z2_9_targetWH: str = "", - - # ---------------- WH3 - 人工堆栈 (Z=3, 15个点位) ---------------- - WH3_x1_y1_z3_1_materialType: str = "", WH3_x1_y1_z3_1_materialId: str = "", WH3_x1_y1_z3_1_quantity: float = 0, - WH3_x2_y1_z3_2_materialType: str = "", WH3_x2_y1_z3_2_materialId: str = "", WH3_x2_y1_z3_2_quantity: float = 0, - WH3_x3_y1_z3_3_materialType: str = "", WH3_x3_y1_z3_3_materialId: str = "", WH3_x3_y1_z3_3_quantity: float = 0, - WH3_x1_y2_z3_4_materialType: str = "", WH3_x1_y2_z3_4_materialId: str = "", WH3_x1_y2_z3_4_quantity: float = 0, - WH3_x2_y2_z3_5_materialType: str = "", WH3_x2_y2_z3_5_materialId: str = "", WH3_x2_y2_z3_5_quantity: float = 0, - WH3_x3_y2_z3_6_materialType: str = "", WH3_x3_y2_z3_6_materialId: str = "", WH3_x3_y2_z3_6_quantity: float = 0, - WH3_x1_y3_z3_7_materialType: str = "", WH3_x1_y3_z3_7_materialId: str = "", WH3_x1_y3_z3_7_quantity: float = 0, - WH3_x2_y3_z3_8_materialType: str = "", WH3_x2_y3_z3_8_materialId: str = "", WH3_x2_y3_z3_8_quantity: float = 0, - WH3_x3_y3_z3_9_materialType: str = "", WH3_x3_y3_z3_9_materialId: str = "", WH3_x3_y3_z3_9_quantity: float = 0, - WH3_x1_y4_z3_10_materialType: str = "", WH3_x1_y4_z3_10_materialId: str = "", WH3_x1_y4_z3_10_quantity: float = 0, - WH3_x2_y4_z3_11_materialType: str = "", WH3_x2_y4_z3_11_materialId: str = "", WH3_x2_y4_z3_11_quantity: float = 0, - WH3_x3_y4_z3_12_materialType: str = "", WH3_x3_y4_z3_12_materialId: str = "", WH3_x3_y4_z3_12_quantity: float = 0, - WH3_x1_y5_z3_13_materialType: str = "", WH3_x1_y5_z3_13_materialId: str = "", WH3_x1_y5_z3_13_quantity: float = 0, - WH3_x2_y5_z3_14_materialType: str = "", WH3_x2_y5_z3_14_materialId: str = "", WH3_x2_y5_z3_14_quantity: float = 0, - WH3_x3_y5_z3_15_materialType: str = "", WH3_x3_y5_z3_15_materialId: str = "", WH3_x3_y5_z3_15_quantity: float = 0, - ) -> Dict[str, Any]: - """ - 组合函数 V2 版本(测试版):先启动调度,然后执行自动化上料 - - ⚠️ 这是测试版本,使用非阻塞轮询等待方式,避免 ROS2 Action feedback publisher 失效 - - 与 V1 的区别: - - 使用 wait_for_order_finish_polling 替代原有的阻塞等待 - - 允许 ROS2 在等待期间正常发布 feedback 消息 - - 适用于长时间运行的任务 - - 参数与 scheduler_start_and_auto_feeding 完全相同 - - Returns: - 包含调度启动结果和上料结果的字典 - """ - logger.info("=" * 60) - logger.info("[V2测试版本] 开始执行组合操作:启动调度 + 自动化上料") - logger.info("=" * 60) - - # 步骤1: 启动调度 - logger.info("【步骤 1/2】启动调度...") - scheduler_result = self.scheduler_start() - logger.info(f"调度启动结果: {scheduler_result}") - - # 检查调度是否启动成功 - if scheduler_result.get("code") != 1: - logger.error(f"调度启动失败: {scheduler_result}") - return { - "success": False, - "step": "scheduler_start", - "scheduler_result": scheduler_result, - "error": "调度启动失败" - } - - logger.info("✓ 调度启动成功") - - # 步骤2: 执行自动化上料(这里会调用 auto_feeding4to3,内部使用轮询等待) - logger.info("【步骤 2/2】执行自动化上料...") - - # 临时替换 wait_for_order_finish 为轮询版本 - original_wait_func = self.wait_for_order_finish - self.wait_for_order_finish = self.wait_for_order_finish_polling - - try: - feeding_result = self.auto_feeding4to3( - xlsx_path=xlsx_path, - WH4_x1_y1_z1_1_materialName=WH4_x1_y1_z1_1_materialName, WH4_x1_y1_z1_1_quantity=WH4_x1_y1_z1_1_quantity, - WH4_x2_y1_z1_2_materialName=WH4_x2_y1_z1_2_materialName, WH4_x2_y1_z1_2_quantity=WH4_x2_y1_z1_2_quantity, - WH4_x3_y1_z1_3_materialName=WH4_x3_y1_z1_3_materialName, WH4_x3_y1_z1_3_quantity=WH4_x3_y1_z1_3_quantity, - WH4_x4_y1_z1_4_materialName=WH4_x4_y1_z1_4_materialName, WH4_x4_y1_z1_4_quantity=WH4_x4_y1_z1_4_quantity, - WH4_x5_y1_z1_5_materialName=WH4_x5_y1_z1_5_materialName, WH4_x5_y1_z1_5_quantity=WH4_x5_y1_z1_5_quantity, - WH4_x1_y2_z1_6_materialName=WH4_x1_y2_z1_6_materialName, WH4_x1_y2_z1_6_quantity=WH4_x1_y2_z1_6_quantity, - WH4_x2_y2_z1_7_materialName=WH4_x2_y2_z1_7_materialName, WH4_x2_y2_z1_7_quantity=WH4_x2_y2_z1_7_quantity, - WH4_x3_y2_z1_8_materialName=WH4_x3_y2_z1_8_materialName, WH4_x3_y2_z1_8_quantity=WH4_x3_y2_z1_8_quantity, - WH4_x4_y2_z1_9_materialName=WH4_x4_y2_z1_9_materialName, WH4_x4_y2_z1_9_quantity=WH4_x4_y2_z1_9_quantity, - WH4_x5_y2_z1_10_materialName=WH4_x5_y2_z1_10_materialName, WH4_x5_y2_z1_10_quantity=WH4_x5_y2_z1_10_quantity, - WH4_x1_y3_z1_11_materialName=WH4_x1_y3_z1_11_materialName, WH4_x1_y3_z1_11_quantity=WH4_x1_y3_z1_11_quantity, - WH4_x2_y3_z1_12_materialName=WH4_x2_y3_z1_12_materialName, WH4_x2_y3_z1_12_quantity=WH4_x2_y3_z1_12_quantity, - WH4_x1_y1_z2_1_materialName=WH4_x1_y1_z2_1_materialName, WH4_x1_y1_z2_1_quantity=WH4_x1_y1_z2_1_quantity, - WH4_x1_y1_z2_1_materialType=WH4_x1_y1_z2_1_materialType, WH4_x1_y1_z2_1_targetWH=WH4_x1_y1_z2_1_targetWH, - WH4_x2_y1_z2_2_materialName=WH4_x2_y1_z2_2_materialName, WH4_x2_y1_z2_2_quantity=WH4_x2_y1_z2_2_quantity, - WH4_x2_y1_z2_2_materialType=WH4_x2_y1_z2_2_materialType, WH4_x2_y1_z2_2_targetWH=WH4_x2_y1_z2_2_targetWH, - WH4_x3_y1_z2_3_materialName=WH4_x3_y1_z2_3_materialName, WH4_x3_y1_z2_3_quantity=WH4_x3_y1_z2_3_quantity, - WH4_x3_y1_z2_3_materialType=WH4_x3_y1_z2_3_materialType, WH4_x3_y1_z2_3_targetWH=WH4_x3_y1_z2_3_targetWH, - WH4_x1_y2_z2_4_materialName=WH4_x1_y2_z2_4_materialName, WH4_x1_y2_z2_4_quantity=WH4_x1_y2_z2_4_quantity, - WH4_x1_y2_z2_4_materialType=WH4_x1_y2_z2_4_materialType, WH4_x1_y2_z2_4_targetWH=WH4_x1_y2_z2_4_targetWH, - WH4_x2_y2_z2_5_materialName=WH4_x2_y2_z2_5_materialName, WH4_x2_y2_z2_5_quantity=WH4_x2_y2_z2_5_quantity, - WH4_x2_y2_z2_5_materialType=WH4_x2_y2_z2_5_materialType, WH4_x2_y2_z2_5_targetWH=WH4_x2_y2_z2_5_targetWH, - WH4_x3_y2_z2_6_materialName=WH4_x3_y2_z2_6_materialName, WH4_x3_y2_z2_6_quantity=WH4_x3_y2_z2_6_quantity, - WH4_x3_y2_z2_6_materialType=WH4_x3_y2_z2_6_materialType, WH4_x3_y2_z2_6_targetWH=WH4_x3_y2_z2_6_targetWH, - WH4_x1_y3_z2_7_materialName=WH4_x1_y3_z2_7_materialName, WH4_x1_y3_z2_7_quantity=WH4_x1_y3_z2_7_quantity, - WH4_x1_y3_z2_7_materialType=WH4_x1_y3_z2_7_materialType, WH4_x1_y3_z2_7_targetWH=WH4_x1_y3_z2_7_targetWH, - WH4_x2_y3_z2_8_materialName=WH4_x2_y3_z2_8_materialName, WH4_x2_y3_z2_8_quantity=WH4_x2_y3_z2_8_quantity, - WH4_x2_y3_z2_8_materialType=WH4_x2_y3_z2_8_materialType, WH4_x2_y3_z2_8_targetWH=WH4_x2_y3_z2_8_targetWH, - WH4_x3_y3_z2_9_materialName=WH4_x3_y3_z2_9_materialName, WH4_x3_y3_z2_9_quantity=WH4_x3_y3_z2_9_quantity, - WH4_x3_y3_z2_9_materialType=WH4_x3_y3_z2_9_materialType, WH4_x3_y3_z2_9_targetWH=WH4_x3_y3_z2_9_targetWH, - WH3_x1_y1_z3_1_materialType=WH3_x1_y1_z3_1_materialType, WH3_x1_y1_z3_1_materialId=WH3_x1_y1_z3_1_materialId, WH3_x1_y1_z3_1_quantity=WH3_x1_y1_z3_1_quantity, - WH3_x2_y1_z3_2_materialType=WH3_x2_y1_z3_2_materialType, WH3_x2_y1_z3_2_materialId=WH3_x2_y1_z3_2_materialId, WH3_x2_y1_z3_2_quantity=WH3_x2_y1_z3_2_quantity, - WH3_x3_y1_z3_3_materialType=WH3_x3_y1_z3_3_materialType, WH3_x3_y1_z3_3_materialId=WH3_x3_y1_z3_3_materialId, WH3_x3_y1_z3_3_quantity=WH3_x3_y1_z3_3_quantity, - WH3_x1_y2_z3_4_materialType=WH3_x1_y2_z3_4_materialType, WH3_x1_y2_z3_4_materialId=WH3_x1_y2_z3_4_materialId, WH3_x1_y2_z3_4_quantity=WH3_x1_y2_z3_4_quantity, - WH3_x2_y2_z3_5_materialType=WH3_x2_y2_z3_5_materialType, WH3_x2_y2_z3_5_materialId=WH3_x2_y2_z3_5_materialId, WH3_x2_y2_z3_5_quantity=WH3_x2_y2_z3_5_quantity, - WH3_x3_y2_z3_6_materialType=WH3_x3_y2_z3_6_materialType, WH3_x3_y2_z3_6_materialId=WH3_x3_y2_z3_6_materialId, WH3_x3_y2_z3_6_quantity=WH3_x3_y2_z3_6_quantity, - WH3_x1_y3_z3_7_materialType=WH3_x1_y3_z3_7_materialType, WH3_x1_y3_z3_7_materialId=WH3_x1_y3_z3_7_materialId, WH3_x1_y3_z3_7_quantity=WH3_x1_y3_z3_7_quantity, - WH3_x2_y3_z3_8_materialType=WH3_x2_y3_z3_8_materialType, WH3_x2_y3_z3_8_materialId=WH3_x2_y3_z3_8_materialId, WH3_x2_y3_z3_8_quantity=WH3_x2_y3_z3_8_quantity, - WH3_x3_y3_z3_9_materialType=WH3_x3_y3_z3_9_materialType, WH3_x3_y3_z3_9_materialId=WH3_x3_y3_z3_9_materialId, WH3_x3_y3_z3_9_quantity=WH3_x3_y3_z3_9_quantity, - WH3_x1_y4_z3_10_materialType=WH3_x1_y4_z3_10_materialType, WH3_x1_y4_z3_10_materialId=WH3_x1_y4_z3_10_materialId, WH3_x1_y4_z3_10_quantity=WH3_x1_y4_z3_10_quantity, - WH3_x2_y4_z3_11_materialType=WH3_x2_y4_z3_11_materialType, WH3_x2_y4_z3_11_materialId=WH3_x2_y4_z3_11_materialId, WH3_x2_y4_z3_11_quantity=WH3_x2_y4_z3_11_quantity, - WH3_x3_y4_z3_12_materialType=WH3_x3_y4_z3_12_materialType, WH3_x3_y4_z3_12_materialId=WH3_x3_y4_z3_12_materialId, WH3_x3_y4_z3_12_quantity=WH3_x3_y4_z3_12_quantity, - WH3_x1_y5_z3_13_materialType=WH3_x1_y5_z3_13_materialType, WH3_x1_y5_z3_13_materialId=WH3_x1_y5_z3_13_materialId, WH3_x1_y5_z3_13_quantity=WH3_x1_y5_z3_13_quantity, - WH3_x2_y5_z3_14_materialType=WH3_x2_y5_z3_14_materialType, WH3_x2_y5_z3_14_materialId=WH3_x2_y5_z3_14_materialId, WH3_x2_y5_z3_14_quantity=WH3_x2_y5_z3_14_quantity, - WH3_x3_y5_z3_15_materialType=WH3_x3_y5_z3_15_materialType, WH3_x3_y5_z3_15_materialId=WH3_x3_y5_z3_15_materialId, WH3_x3_y5_z3_15_quantity=WH3_x3_y5_z3_15_quantity, - ) - finally: - # 恢复原有函数 - self.wait_for_order_finish = original_wait_func - - logger.info("=" * 60) - logger.info("[V2测试版本] 组合操作完成") - logger.info("=" * 60) - - return { - "success": True, - "scheduler_result": scheduler_result, - "feeding_result": feeding_result, - "version": "v2_polling" - } - - # 2.24 物料变更推送 def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]: """ @@ -1956,21 +2410,23 @@ def resource_tree_transfer(self, old_parent: ResourcePLR, plr_resource: Resource if "update_resource_site" in plr_resource.unilabos_extra: site = plr_resource.unilabos_extra["update_resource_site"] plr_model = plr_resource.model - board_type = None - for key, (moudle_name,moudle_uuid) in self.bioyond_config['material_type_mappings'].items(): - if plr_model == moudle_name: - board_type = key - break + + # 直接用 plr_model 作为键查找(配置现在使用英文model名作为键) + board_type = plr_model if plr_model in self.bioyond_config['material_type_mappings'] else None + if board_type is None: - pass + logger.error(f"板类型 {plr_model} 不在 material_type_mappings 中") + return + bottle1 = plr_resource.children[0] - bottle_moudle = bottle1.model - bottle_type = None - for key, (moudle_name, moudle_uuid) in self.bioyond_config['material_type_mappings'].items(): - if bottle_moudle == moudle_name: - bottle_type = key - break + + # 直接用 bottle_moudle 作为键查找 + bottle_type = bottle_moudle if bottle_moudle in self.bioyond_config['material_type_mappings'] else None + + if bottle_type is None: + logger.error(f"瓶类型 {bottle_moudle} 不在 material_type_mappings 中") + return # 从 parent_resource 获取仓库名称 warehouse_name = parent_resource.name if parent_resource else "手动堆栈" @@ -1980,6 +2436,37 @@ def resource_tree_transfer(self, old_parent: ResourcePLR, plr_resource: Resource return self.lab_logger().warning(f"无库位的上料,不处理,{plr_resource} 挂载到 {parent_resource}") + def _get_type_id_by_name(self, type_name: str) -> Optional[str]: + """根据物料类型名称查找对应的 UUID。 + + 查找优先级: + 1. 直接以英文 model 名(如 "YB_Vial_5mL_Carrier")作为 key 查找; + 2. 按中文名称(value[0],如 "5ml分液瓶板")遍历查找。 + + Args: + type_name: 物料类型名称,可以是英文 model key 或中文名称 + + Returns: + 对应的 UUID,如果找不到则返回 None + """ + mappings = self.bioyond_config['material_type_mappings'] + + # 优先:直接 key 命中(英文 model 名) + if type_name in mappings: + value = mappings[type_name] + logger.debug(f"[类型映射] 直接 key 命中: {type_name} → {value[1][:8]}...") + return value[1] + + # 兜底:按中文名遍历(value 格式: [中文名称, UUID]) + for key, value in mappings.items(): + if value[0] == type_name: + logger.debug(f"[类型映射] 中文名匹配: {type_name} → {key} → {value[1][:8]}...") + return value[1] + + logger.error(f"[类型映射] 未找到类型: {type_name}") + logger.debug(f"[类型映射] 可用类型列表: {[v[0] for v in mappings.values()]}") + return None + def create_sample( self, name: str, @@ -1996,8 +2483,14 @@ def create_sample( location_code: 库位编号,例如 "A01" warehouse_name: 仓库名称,默认为 "手动堆栈",支持 "自动堆栈-左"、"自动堆栈-右" 等 """ - carrier_type_id = self.bioyond_config['material_type_mappings'][board_type][1] - bottle_type_id = self.bioyond_config['material_type_mappings'][bottle_type][1] + # 使用反向查找获取 type_id + carrier_type_id = self._get_type_id_by_name(board_type) + bottle_type_id = self._get_type_id_by_name(bottle_type) + + if not carrier_type_id: + raise ValueError(f"未找到板类型 '{board_type}' 的配置,请检查 material_type_mappings") + if not bottle_type_id: + raise ValueError(f"未找到瓶类型 '{bottle_type}' 的配置,请检查 material_type_mappings") # 从指定仓库获取库位UUID if warehouse_name not in self.bioyond_config['warehouse_mapping']: @@ -2052,7 +2545,7 @@ def create_sample( if __name__ == "__main__": lab_registry.setup() - deck = BIOYOND_YB_Deck(setup=True) + deck = bioyond_electrolyte_deck(name="YB_Deck") ws = BioyondCellWorkstation(deck=deck) # ws.create_sample(name="test", board_type="配液瓶(小)板", bottle_type="配液瓶(小)", location_code="B01") # logger.info(ws.scheduler_stop()) diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index 327d8195c..60c18e1e7 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -258,7 +258,7 @@ def sync_to_external(self, resource: Any) -> bool: logger.info(f"[同步→Bioyond] ➕ 物料不存在于 Bioyond,将创建新物料并入库") # 第1步:从配置中获取仓库配置 - warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) + warehouse_mapping = self.workstation.bioyond_config.get("warehouse_mapping", {}) # 确定目标仓库名称 parent_name = None diff --git a/unilabos/devices/workstation/changelog_2026-03-12.md b/unilabos/devices/workstation/changelog_2026-03-12.md new file mode 100644 index 000000000..955954f61 --- /dev/null +++ b/unilabos/devices/workstation/changelog_2026-03-12.md @@ -0,0 +1,219 @@ +# 代码变更说明 — 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` 拖入仓库 E01,carrier 及其子 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 修复 × 3(Container 状态键预填 + 重复 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) | 第一轮 | diff --git a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py index c9187e656..9a1cb2ff5 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py +++ b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py @@ -130,20 +130,14 @@ def __init__( 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: 型号 """ @@ -153,42 +147,45 @@ def __init__( hole_diameter=20.0, info="", ) - # 创建4x4的洞位 - # TODO: 这里要改,对应不同形状 + 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,不在反序列化路径调用)""" + plate = cls(name=name, size_x=size_x, size_y=size_y, size_z=size_z, category=category, model=model) holes = create_ordered_items_2d( klass=MaterialHole, num_items_x=4, num_items_y=4, - dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中 - dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中 + dx=(size_x - 4 * plate._unilabos_state["hole_spacing_x"]) / 2, + dy=(size_y - 4 * plate._unilabos_state["hole_spacing_y"]) / 2, dz=size_z, - 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, + item_dx=plate._unilabos_state["hole_spacing_x"], + item_dy=plate._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, - ) + for hole_name, hole in holes.items(): + plate.assign_child_resource(hole, location=hole.location) + return plate def update_locations(self): # TODO:调多次相加 @@ -534,30 +531,18 @@ def serialize_state(self) -> Dict[str, Dict[str, Any]]: return data -class CoincellDeck(Deck): - """纽扣电池组装工作站台面类""" +class YihuaCoinCellDeck(Deck): + """依华纽扣电池组装工作站台面类""" def __init__( self, name: str = "coin_cell_deck", - size_x: float = 1450.0, # 1m - size_y: float = 1450.0, # 1m - size_z: float = 100.0, # 0.9m + size_x: float = 1450.0, + size_y: float = 1450.0, + size_z: float = 100.0, origin: Coordinate = Coordinate(-2200, 0, 0), category: str = "coin_cell_deck", - 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, @@ -565,8 +550,6 @@ def __init__( size_z=100.0, origin=origin, ) - if setup: - self.setup() def setup(self) -> None: """设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置""" @@ -591,14 +574,11 @@ def setup(self) -> None: # ====================================== 物料板 ============================================ # 创建物料板(料盘carrier)- 4x4布局 # 负极料盘 - fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True) + fujiliaopan = MaterialPlate.create_with_holes(name="负极料盘", size_x=120, size_y=100, size_z=10.0) 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(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0, fill=True) + gemoliaopan = MaterialPlate.create_with_holes(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0) 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) @@ -633,11 +613,27 @@ def setup(self) -> None: 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=""): - cd = CoincellDeck(name=name) - cd.setup() - return cd +def YH_Deck(name: str = "") -> YihuaCoinCellDeck: + return yihua_coin_cell_deck(name=name or "coin_cell_deck") if __name__ == "__main__": 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 91efd45fb..83c7f598c 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -17,7 +17,7 @@ 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 CoincellDeck +from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import YihuaCoinCellDeck, yihua_coin_cell_deck from unilabos.resources.graphio import convert_resources_to_type from unilabos.utils.log import logger import struct @@ -623,12 +623,28 @@ def data_electrolyte_volume(self) -> int: return vol @property - def data_coin_num(self) -> int: - """当前电池数量 (INT16)""" + 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)""" if self.debug_mode: return 0 - num, read_err = self.client.use_node('REG_DATA_COIN_NUM').read(1) - return num + 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 @property def data_coin_cell_code(self) -> str: @@ -726,6 +742,116 @@ def data_glove_box_water_content(self) -> float: 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)""" @@ -1158,7 +1284,8 @@ def func_sendbottle_allpack_multi( lvbodian: bool = True, battery_pressure_mode: bool = True, battery_clean_ignore: bool = False, - file_path: str = "/Users/sml/work" + file_path: str = "/Users/sml/work", + formulations: List[Dict] = None ) -> Dict[str, Any]: """ 发送瓶数+简化组装函数(适用于第二批次及后续批次) @@ -1185,17 +1312,44 @@ def func_sendbottle_allpack_multi( 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 + else: + logger.warning("未接收到配方信息,CSV将不包含配方字段") + self._formulations_map = {} + self._formulations_list = [] + + # ✅ 新增:存储每瓶电池数,用于计算当前使用的瓶号 + # ⚠️ 确保转换为整数(前端可能传递字符串) + 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: 发送电解液瓶数(触发物料搬运) @@ -1331,7 +1485,8 @@ def func_pack_get_msg_cmd(self, file_path: str="D:\\coin_cell_data") -> bool: data_assembly_time = self.data_assembly_time data_assembly_pressure = self.data_assembly_pressure data_electrolyte_volume = self.data_electrolyte_volume - data_coin_num = self.data_coin_num + data_coin_type = self.data_coin_type # 电池类型(7或8种物料) + data_battery_number = self.data_current_assembling_count # ✅ 真正的电池编号 # 处理电解液二维码 - 确保是字符串类型 try: @@ -1361,28 +1516,32 @@ def func_pack_get_msg_cmd(self, file_path: str="D:\\coin_cell_data") -> bool: 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_num: {data_coin_num}") + logger.debug(f"data_coin_type: {data_coin_type}") # 电池类型 + logger.debug(f"data_battery_number: {data_battery_number}") # ✅ 电池编号 logger.debug(f"data_electrolyte_code: {data_electrolyte_code}") logger.debug(f"data_coin_cell_code: {data_coin_cell_code}") #接收完信息后,读取完毕标志位置True - liaopan3 = self.deck.get_resource("成品弹夹") + 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] # 获取目标洞 # 生成唯一的电池名称(使用时间戳确保唯一性) 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 = { @@ -1393,13 +1552,12 @@ def func_pack_get_msg_cmd(self, file_path: str="D:\\coin_cell_data") -> bool: "electrolyte_volume": data_electrolyte_volume } - # 分配新资源到目标位置 + # 将电池堆叠到目标洞中 try: - target_slot.assign_child_resource(battery, location=None) - logger.info(f"成功分配电池 {battery_name} 到位置 {self.coin_num_N}") + target_hole.assign_child_resource(battery, location=None) + logger.info(f"成功放置电池 {battery_name} 到弹夹洞{hole_index}的第{in_hole_position + 1}层 (总计第{self.coin_num_N + 1}颗)") except Exception as e: - logger.error(f"分配电池资源失败: {e}") - # 如果分配失败,尝试使用更简单的方法 + logger.error(f"放置电池资源失败: {e}") raise #print(jipian2.parent) @@ -1430,17 +1588,72 @@ def func_pack_get_msg_cmd(self, file_path: str="D:\\coin_cell_data") -> bool: writer.writerow([ 'Time', 'open_circuit_voltage', 'pole_weight', 'assembly_time', 'assembly_pressure', 'electrolyte_volume', - 'coin_num', 'electrolyte_code', 'coin_cell_code' + 'coin_num', 'electrolyte_code', 'coin_cell_code', + 'formulation_order_code', 'formulation_ratio' # ← 新增配方列 ]) #立刻写入磁盘 csvfile.flush() #开始追加电池信息 with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile: writer = csv.writer(csvfile) + + # ========== 提取配方信息 ========== + formulation_order_code = "" + formulation_ratio_str = "" + + # 从 self._formulations_list 获取配方信息 + if hasattr(self, '_formulations_list') and self._formulations_list: + # ✅ 新方案:根据电池编号和每瓶电池数计算当前瓶号 + # 例如:elec_use_num=2时,电池1-2用瓶0,电池3-4用瓶1 + 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] + formulation_order_code = formulation.get("orderCode", "") + # ✅ 优先使用实际质量比(real_mass_ratio),如果不存在则使用目标质量比 + real_ratio = formulation.get("real_mass_ratio", {}) + target_ratio = formulation.get("target_mass_ratio", {}) + mass_ratio = real_ratio if real_ratio else target_ratio + + # 将配方比例转为JSON字符串 + import json + formulation_ratio_str = json.dumps(mass_ratio, ensure_ascii=False) if mass_ratio else "" + + logger.info( + f"[CSV写入] 电池 {data_battery_number}: 使用配方[{current_bottle_index}] " + f"orderCode={formulation_order_code}, 比例={formulation_ratio_str}" + ) + else: + logger.warning( + f"[CSV写入] 电池 {data_battery_number}: 瓶号索引 {current_bottle_index} " + f"超出配方列表范围 (共{len(self._formulations_list)}个配方)" + ) + 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_num, data_electrolyte_code, data_coin_cell_code + data_coin_type, data_electrolyte_code, data_coin_cell_code, + formulation_order_code, formulation_ratio_str # ← 新增配方数据 ]) #立刻写入磁盘 csvfile.flush() @@ -1667,8 +1880,7 @@ def func_allpack_cmd_simp( file_path: str = "/Users/sml/work" ) -> Dict[str, Any]: """ - 简化版电池组装函数,整合了原 qiming_coin_cell_code 的参数设置和双滴模式 - + 此函数是 func_allpack_cmd 的增强版本,自动处理以下配置: - 负极片和隔膜的盘数及矩阵点位 - 枪头盒矩阵点位 @@ -1922,7 +2134,7 @@ def func_pack_device_stop(self) -> bool: def fun_wuliao_test(self) -> bool: #找到data_init中构建的2个物料盘 - liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8") + test_battery_plate = 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 = { @@ -1932,7 +2144,7 @@ def fun_wuliao_test(self) -> bool: "electrolyte_volume": 20.0, "electrolyte_name": f"DP{i}" } - liaopan3.children[i].assign_child_resource(battery, location=None) + test_battery_plate.children[i].assign_child_resource(battery, location=None) ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ "resources": [self.deck] @@ -1975,7 +2187,7 @@ def func_read_data_and_output(self, file_path: str="/Users/sml/work"): data_assembly_time = self.data_assembly_time data_assembly_pressure = self.data_assembly_pressure data_electrolyte_volume = self.data_electrolyte_volume - data_coin_num = self.data_coin_num + data_coin_type = self.data_coin_type # 电池类型(7或8种物料) data_electrolyte_code = self.data_electrolyte_code data_coin_cell_code = self.data_coin_cell_code # 电解液瓶位置 @@ -2089,7 +2301,7 @@ def func_read_data_and_output(self, file_path: str="/Users/sml/work"): writer.writerow([ timestamp, data_open_circuit_voltage, data_pole_weight, data_assembly_time, data_assembly_pressure, data_electrolyte_volume, - data_coin_num, data_electrolyte_code, data_coin_cell_code + data_coin_type, data_electrolyte_code, data_coin_cell_code # ✅ 已修正 ]) #立刻写入磁盘 csvfile.flush() @@ -2140,7 +2352,7 @@ def data_tips_inventory(self) -> int: if __name__ == "__main__": # 简单测试 - workstation = CoinCellAssemblyWorkstation(deck=CoincellDeck(setup=True, name="coin_cell_deck")) + workstation = CoinCellAssemblyWorkstation(deck=yihua_coin_cell_deck(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)}") diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv index e46d1de5f..d28b1b6d1 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv @@ -1,4 +1,4 @@ -Name,DataType,InitValue,Comment,Attribute,DeviceType,Address, +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, @@ -29,7 +29,9 @@ 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_COIN_TYPE,INT16,,,,hold_register,10018,data_coin_type +REG_DATA_CURRENT_ASSEMBLING_COUNT,INT16,,,,hold_register,10072,data_current_assembling_count +REG_DATA_CURRENT_COMPLETED_COUNT,INT16,,,,hold_register,10074,data_current_completed_count 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() @@ -69,65 +71,75 @@ 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-电解液瓶盖在籍异常 +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-????????? +REG_DATA_10MM_POSITIVE_PLATE_REMAINING_COUNT,FLOAT32,,,,hold_register,520,10mm??????????R? +REG_DATA_12MM_POSITIVE_PLATE_REMAINING_COUNT,FLOAT32,,,,hold_register,522,12mm??????????R? +REG_DATA_16MM_POSITIVE_PLATE_REMAINING_COUNT,FLOAT32,,,,hold_register,524,16mm??????????R? +REG_DATA_ALUMINUM_FOIL_REMAINING_COUNT,FLOAT32,,,,hold_register,526,?????????R? +REG_DATA_POSITIVE_SHELL_REMAINING_COUNT,FLOAT32,,,,hold_register,528,??????????R? +REG_DATA_FLAT_WASHER_REMAINING_COUNT,FLOAT32,,,,hold_register,530,?????????R? +REG_DATA_NEGATIVE_SHELL_REMAINING_COUNT,FLOAT32,,,,hold_register,532,??????????R? +REG_DATA_SPRING_WASHER_REMAINING_COUNT,FLOAT32,,,,hold_register,534,?????????R? +REG_DATA_FINISHED_BATTERY_REMAINING_CAPACITY,FLOAT32,,,,hold_register,536,????????????R? +REG_DATA_FINISHED_BATTERY_NG_REMAINING_CAPACITY,FLOAT32,,,,hold_register,538,????NG?????????R? diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index fc4b75cb2..aa6abd96a 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -32,111 +32,6 @@ bioyond_cell: feedback: {} goal: {} goal_default: - WH3_x1_y1_z3_1_materialId: '' - WH3_x1_y1_z3_1_materialType: '' - WH3_x1_y1_z3_1_quantity: 0 - WH3_x1_y2_z3_4_materialId: '' - WH3_x1_y2_z3_4_materialType: '' - WH3_x1_y2_z3_4_quantity: 0 - WH3_x1_y3_z3_7_materialId: '' - WH3_x1_y3_z3_7_materialType: '' - WH3_x1_y3_z3_7_quantity: 0 - WH3_x1_y4_z3_10_materialId: '' - WH3_x1_y4_z3_10_materialType: '' - WH3_x1_y4_z3_10_quantity: 0 - WH3_x1_y5_z3_13_materialId: '' - WH3_x1_y5_z3_13_materialType: '' - WH3_x1_y5_z3_13_quantity: 0 - WH3_x2_y1_z3_2_materialId: '' - WH3_x2_y1_z3_2_materialType: '' - WH3_x2_y1_z3_2_quantity: 0 - WH3_x2_y2_z3_5_materialId: '' - WH3_x2_y2_z3_5_materialType: '' - WH3_x2_y2_z3_5_quantity: 0 - WH3_x2_y3_z3_8_materialId: '' - WH3_x2_y3_z3_8_materialType: '' - WH3_x2_y3_z3_8_quantity: 0 - WH3_x2_y4_z3_11_materialId: '' - WH3_x2_y4_z3_11_materialType: '' - WH3_x2_y4_z3_11_quantity: 0 - WH3_x2_y5_z3_14_materialId: '' - WH3_x2_y5_z3_14_materialType: '' - WH3_x2_y5_z3_14_quantity: 0 - WH3_x3_y1_z3_3_materialId: '' - WH3_x3_y1_z3_3_materialType: '' - WH3_x3_y1_z3_3_quantity: 0 - WH3_x3_y2_z3_6_materialId: '' - WH3_x3_y2_z3_6_materialType: '' - WH3_x3_y2_z3_6_quantity: 0 - WH3_x3_y3_z3_9_materialId: '' - WH3_x3_y3_z3_9_materialType: '' - WH3_x3_y3_z3_9_quantity: 0 - WH3_x3_y4_z3_12_materialId: '' - WH3_x3_y4_z3_12_materialType: '' - WH3_x3_y4_z3_12_quantity: 0 - WH3_x3_y5_z3_15_materialId: '' - WH3_x3_y5_z3_15_materialType: '' - WH3_x3_y5_z3_15_quantity: 0 - WH4_x1_y1_z1_1_materialName: '' - WH4_x1_y1_z1_1_quantity: 0.0 - WH4_x1_y1_z2_1_materialName: '' - WH4_x1_y1_z2_1_materialType: '' - WH4_x1_y1_z2_1_quantity: 0.0 - WH4_x1_y1_z2_1_targetWH: '' - WH4_x1_y2_z1_6_materialName: '' - WH4_x1_y2_z1_6_quantity: 0.0 - WH4_x1_y2_z2_4_materialName: '' - WH4_x1_y2_z2_4_materialType: '' - WH4_x1_y2_z2_4_quantity: 0.0 - WH4_x1_y2_z2_4_targetWH: '' - WH4_x1_y3_z1_11_materialName: '' - WH4_x1_y3_z1_11_quantity: 0.0 - WH4_x1_y3_z2_7_materialName: '' - WH4_x1_y3_z2_7_materialType: '' - WH4_x1_y3_z2_7_quantity: 0.0 - WH4_x1_y3_z2_7_targetWH: '' - WH4_x2_y1_z1_2_materialName: '' - WH4_x2_y1_z1_2_quantity: 0.0 - WH4_x2_y1_z2_2_materialName: '' - WH4_x2_y1_z2_2_materialType: '' - WH4_x2_y1_z2_2_quantity: 0.0 - WH4_x2_y1_z2_2_targetWH: '' - WH4_x2_y2_z1_7_materialName: '' - WH4_x2_y2_z1_7_quantity: 0.0 - WH4_x2_y2_z2_5_materialName: '' - WH4_x2_y2_z2_5_materialType: '' - WH4_x2_y2_z2_5_quantity: 0.0 - WH4_x2_y2_z2_5_targetWH: '' - WH4_x2_y3_z1_12_materialName: '' - WH4_x2_y3_z1_12_quantity: 0.0 - WH4_x2_y3_z2_8_materialName: '' - WH4_x2_y3_z2_8_materialType: '' - WH4_x2_y3_z2_8_quantity: 0.0 - WH4_x2_y3_z2_8_targetWH: '' - WH4_x3_y1_z1_3_materialName: '' - WH4_x3_y1_z1_3_quantity: 0.0 - WH4_x3_y1_z2_3_materialName: '' - WH4_x3_y1_z2_3_materialType: '' - WH4_x3_y1_z2_3_quantity: 0.0 - WH4_x3_y1_z2_3_targetWH: '' - WH4_x3_y2_z1_8_materialName: '' - WH4_x3_y2_z1_8_quantity: 0.0 - WH4_x3_y2_z2_6_materialName: '' - WH4_x3_y2_z2_6_materialType: '' - WH4_x3_y2_z2_6_quantity: 0.0 - WH4_x3_y2_z2_6_targetWH: '' - WH4_x3_y3_z2_9_materialName: '' - WH4_x3_y3_z2_9_materialType: '' - WH4_x3_y3_z2_9_quantity: 0.0 - WH4_x3_y3_z2_9_targetWH: '' - WH4_x4_y1_z1_4_materialName: '' - WH4_x4_y1_z1_4_quantity: 0.0 - WH4_x4_y2_z1_9_materialName: '' - WH4_x4_y2_z1_9_quantity: 0.0 - WH4_x5_y1_z1_5_materialName: '' - WH4_x5_y1_z1_5_quantity: 0.0 - WH4_x5_y2_z1_10_materialName: '' - WH4_x5_y2_z1_10_quantity: 0.0 xlsx_path: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx handles: {} placeholder_keys: {} @@ -147,321 +42,6 @@ bioyond_cell: feedback: {} goal: properties: - WH3_x1_y1_z3_1_materialId: - default: '' - type: string - WH3_x1_y1_z3_1_materialType: - default: '' - type: string - WH3_x1_y1_z3_1_quantity: - default: 0 - type: number - WH3_x1_y2_z3_4_materialId: - default: '' - type: string - WH3_x1_y2_z3_4_materialType: - default: '' - type: string - WH3_x1_y2_z3_4_quantity: - default: 0 - type: number - WH3_x1_y3_z3_7_materialId: - default: '' - type: string - WH3_x1_y3_z3_7_materialType: - default: '' - type: string - WH3_x1_y3_z3_7_quantity: - default: 0 - type: number - WH3_x1_y4_z3_10_materialId: - default: '' - type: string - WH3_x1_y4_z3_10_materialType: - default: '' - type: string - WH3_x1_y4_z3_10_quantity: - default: 0 - type: number - WH3_x1_y5_z3_13_materialId: - default: '' - type: string - WH3_x1_y5_z3_13_materialType: - default: '' - type: string - WH3_x1_y5_z3_13_quantity: - default: 0 - type: number - WH3_x2_y1_z3_2_materialId: - default: '' - type: string - WH3_x2_y1_z3_2_materialType: - default: '' - type: string - WH3_x2_y1_z3_2_quantity: - default: 0 - type: number - WH3_x2_y2_z3_5_materialId: - default: '' - type: string - WH3_x2_y2_z3_5_materialType: - default: '' - type: string - WH3_x2_y2_z3_5_quantity: - default: 0 - type: number - WH3_x2_y3_z3_8_materialId: - default: '' - type: string - WH3_x2_y3_z3_8_materialType: - default: '' - type: string - WH3_x2_y3_z3_8_quantity: - default: 0 - type: number - WH3_x2_y4_z3_11_materialId: - default: '' - type: string - WH3_x2_y4_z3_11_materialType: - default: '' - type: string - WH3_x2_y4_z3_11_quantity: - default: 0 - type: number - WH3_x2_y5_z3_14_materialId: - default: '' - type: string - WH3_x2_y5_z3_14_materialType: - default: '' - type: string - WH3_x2_y5_z3_14_quantity: - default: 0 - type: number - WH3_x3_y1_z3_3_materialId: - default: '' - type: string - WH3_x3_y1_z3_3_materialType: - default: '' - type: string - WH3_x3_y1_z3_3_quantity: - default: 0 - type: number - WH3_x3_y2_z3_6_materialId: - default: '' - type: string - WH3_x3_y2_z3_6_materialType: - default: '' - type: string - WH3_x3_y2_z3_6_quantity: - default: 0 - type: number - WH3_x3_y3_z3_9_materialId: - default: '' - type: string - WH3_x3_y3_z3_9_materialType: - default: '' - type: string - WH3_x3_y3_z3_9_quantity: - default: 0 - type: number - WH3_x3_y4_z3_12_materialId: - default: '' - type: string - WH3_x3_y4_z3_12_materialType: - default: '' - type: string - WH3_x3_y4_z3_12_quantity: - default: 0 - type: number - WH3_x3_y5_z3_15_materialId: - default: '' - type: string - WH3_x3_y5_z3_15_materialType: - default: '' - type: string - WH3_x3_y5_z3_15_quantity: - default: 0 - type: number - WH4_x1_y1_z1_1_materialName: - default: '' - type: string - WH4_x1_y1_z1_1_quantity: - default: 0.0 - type: number - WH4_x1_y1_z2_1_materialName: - default: '' - type: string - WH4_x1_y1_z2_1_materialType: - default: '' - type: string - WH4_x1_y1_z2_1_quantity: - default: 0.0 - type: number - WH4_x1_y1_z2_1_targetWH: - default: '' - type: string - WH4_x1_y2_z1_6_materialName: - default: '' - type: string - WH4_x1_y2_z1_6_quantity: - default: 0.0 - type: number - WH4_x1_y2_z2_4_materialName: - default: '' - type: string - WH4_x1_y2_z2_4_materialType: - default: '' - type: string - WH4_x1_y2_z2_4_quantity: - default: 0.0 - type: number - WH4_x1_y2_z2_4_targetWH: - default: '' - type: string - WH4_x1_y3_z1_11_materialName: - default: '' - type: string - WH4_x1_y3_z1_11_quantity: - default: 0.0 - type: number - WH4_x1_y3_z2_7_materialName: - default: '' - type: string - WH4_x1_y3_z2_7_materialType: - default: '' - type: string - WH4_x1_y3_z2_7_quantity: - default: 0.0 - type: number - WH4_x1_y3_z2_7_targetWH: - default: '' - type: string - WH4_x2_y1_z1_2_materialName: - default: '' - type: string - WH4_x2_y1_z1_2_quantity: - default: 0.0 - type: number - WH4_x2_y1_z2_2_materialName: - default: '' - type: string - WH4_x2_y1_z2_2_materialType: - default: '' - type: string - WH4_x2_y1_z2_2_quantity: - default: 0.0 - type: number - WH4_x2_y1_z2_2_targetWH: - default: '' - type: string - WH4_x2_y2_z1_7_materialName: - default: '' - type: string - WH4_x2_y2_z1_7_quantity: - default: 0.0 - type: number - WH4_x2_y2_z2_5_materialName: - default: '' - type: string - WH4_x2_y2_z2_5_materialType: - default: '' - type: string - WH4_x2_y2_z2_5_quantity: - default: 0.0 - type: number - WH4_x2_y2_z2_5_targetWH: - default: '' - type: string - WH4_x2_y3_z1_12_materialName: - default: '' - type: string - WH4_x2_y3_z1_12_quantity: - default: 0.0 - type: number - WH4_x2_y3_z2_8_materialName: - default: '' - type: string - WH4_x2_y3_z2_8_materialType: - default: '' - type: string - WH4_x2_y3_z2_8_quantity: - default: 0.0 - type: number - WH4_x2_y3_z2_8_targetWH: - default: '' - type: string - WH4_x3_y1_z1_3_materialName: - default: '' - type: string - WH4_x3_y1_z1_3_quantity: - default: 0.0 - type: number - WH4_x3_y1_z2_3_materialName: - default: '' - type: string - WH4_x3_y1_z2_3_materialType: - default: '' - type: string - WH4_x3_y1_z2_3_quantity: - default: 0.0 - type: number - WH4_x3_y1_z2_3_targetWH: - default: '' - type: string - WH4_x3_y2_z1_8_materialName: - default: '' - type: string - WH4_x3_y2_z1_8_quantity: - default: 0.0 - type: number - WH4_x3_y2_z2_6_materialName: - default: '' - type: string - WH4_x3_y2_z2_6_materialType: - default: '' - type: string - WH4_x3_y2_z2_6_quantity: - default: 0.0 - type: number - WH4_x3_y2_z2_6_targetWH: - default: '' - type: string - WH4_x3_y3_z2_9_materialName: - default: '' - type: string - WH4_x3_y3_z2_9_materialType: - default: '' - type: string - WH4_x3_y3_z2_9_quantity: - default: 0.0 - type: number - WH4_x3_y3_z2_9_targetWH: - default: '' - type: string - WH4_x4_y1_z1_4_materialName: - default: '' - type: string - WH4_x4_y1_z1_4_quantity: - default: 0.0 - type: number - WH4_x4_y2_z1_9_materialName: - default: '' - type: string - WH4_x4_y2_z1_9_quantity: - default: 0.0 - type: number - WH4_x5_y1_z1_5_materialName: - default: '' - type: string - WH4_x5_y1_z1_5_quantity: - default: 0.0 - type: number - WH4_x5_y2_z1_10_materialName: - default: '' - type: string - WH4_x5_y2_z1_10_quantity: - default: 0.0 - type: number xlsx_path: default: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx type: string @@ -567,6 +147,7 @@ bioyond_cell: type: object type: UniLabJsonCommand auto-create_orders: + always_free: true feedback: {} goal: {} goal_default: @@ -577,8 +158,17 @@ bioyond_cell: data_source: executor data_type: integer handler_key: bottle_count - io_type: sink label: 配液瓶数 + - data_key: vial_plates + data_source: executor + data_type: array + handler_key: vial_plates_output + label: 分液瓶板列表 + - data_key: mass_ratios + data_source: executor + data_type: array + handler_key: mass_ratios_output + label: 配方信息列表 placeholder_keys: {} result: {} schema: @@ -598,36 +188,106 @@ bioyond_cell: title: create_orders参数 type: object type: UniLabJsonCommand - auto-create_orders_v2: + auto-create_orders_formulation: + always_free: true feedback: {} goal: {} goal_default: - xlsx_path: null + batch_id: '' + bottle_type: 配液小瓶 + conductivity_bottle_count: 0 + conductivity_info: 0.0 + formulation: null + load_shedding_info: 0.0 + mix_time: 0 + pouch_cell_info: 0.0 handles: output: - data_key: total_orders data_source: executor data_type: integer handler_key: bottle_count - io_type: sink label: 配液瓶数 - placeholder_keys: {} + - data_key: vial_plates + data_source: executor + data_type: array + handler_key: vial_plates_output + label: 分液瓶板列表 + - data_key: mass_ratios + data_source: executor + data_type: array + handler_key: mass_ratios_output + label: 配方信息列表 + placeholder_keys: + formulation: unilabos_formulation result: {} schema: - description: 从Excel解析并创建实验(V2版本) + description: 配方批量输入版本的创建实验——通过前端配方组件输入物料配比,替代Excel导入 properties: feedback: {} goal: properties: - xlsx_path: + batch_id: + default: '' + description: 批次ID,为空则自动生成时间戳 + type: string + bottle_type: + default: 配液小瓶 + description: 配液瓶类型 type: string + conductivity_bottle_count: + default: 0 + description: 电导测试分液瓶数 + type: integer + conductivity_info: + default: 0.0 + description: 电导测试分液体积 + type: number + formulation: + description: 配方列表,每个元素代表一个订单(一瓶) + items: + properties: + materials: + description: 物料列表 + items: + properties: + mass: + description: 质量(g) + type: number + name: + description: 物料名称 + type: string + required: + - name + - mass + type: object + type: array + order_name: + description: 配方名称(可选) + type: string + required: + - materials + type: object + type: array + load_shedding_info: + default: 0.0 + description: 扣电组装分液体积 + type: number + mix_time: + default: 0 + description: 混匀时间(秒) + type: integer + pouch_cell_info: + default: 0.0 + description: 软包组装分液体积 + type: number required: - - xlsx_path + - formulation type: object result: {} required: - goal - title: create_orders_v2参数 + title: create_orders_formulation参数 type: object type: UniLabJsonCommand auto-create_sample: @@ -927,111 +587,6 @@ bioyond_cell: feedback: {} goal: {} goal_default: - WH3_x1_y1_z3_1_materialId: '' - WH3_x1_y1_z3_1_materialType: '' - WH3_x1_y1_z3_1_quantity: 0 - WH3_x1_y2_z3_4_materialId: '' - WH3_x1_y2_z3_4_materialType: '' - WH3_x1_y2_z3_4_quantity: 0 - WH3_x1_y3_z3_7_materialId: '' - WH3_x1_y3_z3_7_materialType: '' - WH3_x1_y3_z3_7_quantity: 0 - WH3_x1_y4_z3_10_materialId: '' - WH3_x1_y4_z3_10_materialType: '' - WH3_x1_y4_z3_10_quantity: 0 - WH3_x1_y5_z3_13_materialId: '' - WH3_x1_y5_z3_13_materialType: '' - WH3_x1_y5_z3_13_quantity: 0 - WH3_x2_y1_z3_2_materialId: '' - WH3_x2_y1_z3_2_materialType: '' - WH3_x2_y1_z3_2_quantity: 0 - WH3_x2_y2_z3_5_materialId: '' - WH3_x2_y2_z3_5_materialType: '' - WH3_x2_y2_z3_5_quantity: 0 - WH3_x2_y3_z3_8_materialId: '' - WH3_x2_y3_z3_8_materialType: '' - WH3_x2_y3_z3_8_quantity: 0 - WH3_x2_y4_z3_11_materialId: '' - WH3_x2_y4_z3_11_materialType: '' - WH3_x2_y4_z3_11_quantity: 0 - WH3_x2_y5_z3_14_materialId: '' - WH3_x2_y5_z3_14_materialType: '' - WH3_x2_y5_z3_14_quantity: 0 - WH3_x3_y1_z3_3_materialId: '' - WH3_x3_y1_z3_3_materialType: '' - WH3_x3_y1_z3_3_quantity: 0 - WH3_x3_y2_z3_6_materialId: '' - WH3_x3_y2_z3_6_materialType: '' - WH3_x3_y2_z3_6_quantity: 0 - WH3_x3_y3_z3_9_materialId: '' - WH3_x3_y3_z3_9_materialType: '' - WH3_x3_y3_z3_9_quantity: 0 - WH3_x3_y4_z3_12_materialId: '' - WH3_x3_y4_z3_12_materialType: '' - WH3_x3_y4_z3_12_quantity: 0 - WH3_x3_y5_z3_15_materialId: '' - WH3_x3_y5_z3_15_materialType: '' - WH3_x3_y5_z3_15_quantity: 0 - WH4_x1_y1_z1_1_materialName: '' - WH4_x1_y1_z1_1_quantity: 0.0 - WH4_x1_y1_z2_1_materialName: '' - WH4_x1_y1_z2_1_materialType: '' - WH4_x1_y1_z2_1_quantity: 0.0 - WH4_x1_y1_z2_1_targetWH: '' - WH4_x1_y2_z1_6_materialName: '' - WH4_x1_y2_z1_6_quantity: 0.0 - WH4_x1_y2_z2_4_materialName: '' - WH4_x1_y2_z2_4_materialType: '' - WH4_x1_y2_z2_4_quantity: 0.0 - WH4_x1_y2_z2_4_targetWH: '' - WH4_x1_y3_z1_11_materialName: '' - WH4_x1_y3_z1_11_quantity: 0.0 - WH4_x1_y3_z2_7_materialName: '' - WH4_x1_y3_z2_7_materialType: '' - WH4_x1_y3_z2_7_quantity: 0.0 - WH4_x1_y3_z2_7_targetWH: '' - WH4_x2_y1_z1_2_materialName: '' - WH4_x2_y1_z1_2_quantity: 0.0 - WH4_x2_y1_z2_2_materialName: '' - WH4_x2_y1_z2_2_materialType: '' - WH4_x2_y1_z2_2_quantity: 0.0 - WH4_x2_y1_z2_2_targetWH: '' - WH4_x2_y2_z1_7_materialName: '' - WH4_x2_y2_z1_7_quantity: 0.0 - WH4_x2_y2_z2_5_materialName: '' - WH4_x2_y2_z2_5_materialType: '' - WH4_x2_y2_z2_5_quantity: 0.0 - WH4_x2_y2_z2_5_targetWH: '' - WH4_x2_y3_z1_12_materialName: '' - WH4_x2_y3_z1_12_quantity: 0.0 - WH4_x2_y3_z2_8_materialName: '' - WH4_x2_y3_z2_8_materialType: '' - WH4_x2_y3_z2_8_quantity: 0.0 - WH4_x2_y3_z2_8_targetWH: '' - WH4_x3_y1_z1_3_materialName: '' - WH4_x3_y1_z1_3_quantity: 0.0 - WH4_x3_y1_z2_3_materialName: '' - WH4_x3_y1_z2_3_materialType: '' - WH4_x3_y1_z2_3_quantity: 0.0 - WH4_x3_y1_z2_3_targetWH: '' - WH4_x3_y2_z1_8_materialName: '' - WH4_x3_y2_z1_8_quantity: 0.0 - WH4_x3_y2_z2_6_materialName: '' - WH4_x3_y2_z2_6_materialType: '' - WH4_x3_y2_z2_6_quantity: 0.0 - WH4_x3_y2_z2_6_targetWH: '' - WH4_x3_y3_z2_9_materialName: '' - WH4_x3_y3_z2_9_materialType: '' - WH4_x3_y3_z2_9_quantity: 0.0 - WH4_x3_y3_z2_9_targetWH: '' - WH4_x4_y1_z1_4_materialName: '' - WH4_x4_y1_z1_4_quantity: 0.0 - WH4_x4_y2_z1_9_materialName: '' - WH4_x4_y2_z1_9_quantity: 0.0 - WH4_x5_y1_z1_5_materialName: '' - WH4_x5_y1_z1_5_quantity: 0.0 - WH4_x5_y2_z1_10_materialName: '' - WH4_x5_y2_z1_10_quantity: 0.0 xlsx_path: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx handles: {} placeholder_keys: {} @@ -1042,323 +597,8 @@ bioyond_cell: feedback: {} goal: properties: - WH3_x1_y1_z3_1_materialId: - default: '' - type: string - WH3_x1_y1_z3_1_materialType: - default: '' - type: string - WH3_x1_y1_z3_1_quantity: - default: 0 - type: number - WH3_x1_y2_z3_4_materialId: - default: '' - type: string - WH3_x1_y2_z3_4_materialType: - default: '' - type: string - WH3_x1_y2_z3_4_quantity: - default: 0 - type: number - WH3_x1_y3_z3_7_materialId: - default: '' - type: string - WH3_x1_y3_z3_7_materialType: - default: '' - type: string - WH3_x1_y3_z3_7_quantity: - default: 0 - type: number - WH3_x1_y4_z3_10_materialId: - default: '' - type: string - WH3_x1_y4_z3_10_materialType: - default: '' - type: string - WH3_x1_y4_z3_10_quantity: - default: 0 - type: number - WH3_x1_y5_z3_13_materialId: - default: '' - type: string - WH3_x1_y5_z3_13_materialType: - default: '' - type: string - WH3_x1_y5_z3_13_quantity: - default: 0 - type: number - WH3_x2_y1_z3_2_materialId: - default: '' - type: string - WH3_x2_y1_z3_2_materialType: - default: '' - type: string - WH3_x2_y1_z3_2_quantity: - default: 0 - type: number - WH3_x2_y2_z3_5_materialId: - default: '' - type: string - WH3_x2_y2_z3_5_materialType: - default: '' - type: string - WH3_x2_y2_z3_5_quantity: - default: 0 - type: number - WH3_x2_y3_z3_8_materialId: - default: '' - type: string - WH3_x2_y3_z3_8_materialType: - default: '' - type: string - WH3_x2_y3_z3_8_quantity: - default: 0 - type: number - WH3_x2_y4_z3_11_materialId: - default: '' - type: string - WH3_x2_y4_z3_11_materialType: - default: '' - type: string - WH3_x2_y4_z3_11_quantity: - default: 0 - type: number - WH3_x2_y5_z3_14_materialId: - default: '' - type: string - WH3_x2_y5_z3_14_materialType: - default: '' - type: string - WH3_x2_y5_z3_14_quantity: - default: 0 - type: number - WH3_x3_y1_z3_3_materialId: - default: '' - type: string - WH3_x3_y1_z3_3_materialType: - default: '' - type: string - WH3_x3_y1_z3_3_quantity: - default: 0 - type: number - WH3_x3_y2_z3_6_materialId: - default: '' - type: string - WH3_x3_y2_z3_6_materialType: - default: '' - type: string - WH3_x3_y2_z3_6_quantity: - default: 0 - type: number - WH3_x3_y3_z3_9_materialId: - default: '' - type: string - WH3_x3_y3_z3_9_materialType: - default: '' - type: string - WH3_x3_y3_z3_9_quantity: - default: 0 - type: number - WH3_x3_y4_z3_12_materialId: - default: '' - type: string - WH3_x3_y4_z3_12_materialType: - default: '' - type: string - WH3_x3_y4_z3_12_quantity: - default: 0 - type: number - WH3_x3_y5_z3_15_materialId: - default: '' - type: string - WH3_x3_y5_z3_15_materialType: - default: '' - type: string - WH3_x3_y5_z3_15_quantity: - default: 0 - type: number - WH4_x1_y1_z1_1_materialName: - default: '' - type: string - WH4_x1_y1_z1_1_quantity: - default: 0.0 - type: number - WH4_x1_y1_z2_1_materialName: - default: '' - type: string - WH4_x1_y1_z2_1_materialType: - default: '' - type: string - WH4_x1_y1_z2_1_quantity: - default: 0.0 - type: number - WH4_x1_y1_z2_1_targetWH: - default: '' - type: string - WH4_x1_y2_z1_6_materialName: - default: '' - type: string - WH4_x1_y2_z1_6_quantity: - default: 0.0 - type: number - WH4_x1_y2_z2_4_materialName: - default: '' - type: string - WH4_x1_y2_z2_4_materialType: - default: '' - type: string - WH4_x1_y2_z2_4_quantity: - default: 0.0 - type: number - WH4_x1_y2_z2_4_targetWH: - default: '' - type: string - WH4_x1_y3_z1_11_materialName: - default: '' - type: string - WH4_x1_y3_z1_11_quantity: - default: 0.0 - type: number - WH4_x1_y3_z2_7_materialName: - default: '' - type: string - WH4_x1_y3_z2_7_materialType: - default: '' - type: string - WH4_x1_y3_z2_7_quantity: - default: 0.0 - type: number - WH4_x1_y3_z2_7_targetWH: - default: '' - type: string - WH4_x2_y1_z1_2_materialName: - default: '' - type: string - WH4_x2_y1_z1_2_quantity: - default: 0.0 - type: number - WH4_x2_y1_z2_2_materialName: - default: '' - type: string - WH4_x2_y1_z2_2_materialType: - default: '' - type: string - WH4_x2_y1_z2_2_quantity: - default: 0.0 - type: number - WH4_x2_y1_z2_2_targetWH: - default: '' - type: string - WH4_x2_y2_z1_7_materialName: - default: '' - type: string - WH4_x2_y2_z1_7_quantity: - default: 0.0 - type: number - WH4_x2_y2_z2_5_materialName: - default: '' - type: string - WH4_x2_y2_z2_5_materialType: - default: '' - type: string - WH4_x2_y2_z2_5_quantity: - default: 0.0 - type: number - WH4_x2_y2_z2_5_targetWH: - default: '' - type: string - WH4_x2_y3_z1_12_materialName: - default: '' - type: string - WH4_x2_y3_z1_12_quantity: - default: 0.0 - type: number - WH4_x2_y3_z2_8_materialName: - default: '' - type: string - WH4_x2_y3_z2_8_materialType: - default: '' - type: string - WH4_x2_y3_z2_8_quantity: - default: 0.0 - type: number - WH4_x2_y3_z2_8_targetWH: - default: '' - type: string - WH4_x3_y1_z1_3_materialName: - default: '' - type: string - WH4_x3_y1_z1_3_quantity: - default: 0.0 - type: number - WH4_x3_y1_z2_3_materialName: - default: '' - type: string - WH4_x3_y1_z2_3_materialType: - default: '' - type: string - WH4_x3_y1_z2_3_quantity: - default: 0.0 - type: number - WH4_x3_y1_z2_3_targetWH: - default: '' - type: string - WH4_x3_y2_z1_8_materialName: - default: '' - type: string - WH4_x3_y2_z1_8_quantity: - default: 0.0 - type: number - WH4_x3_y2_z2_6_materialName: - default: '' - type: string - WH4_x3_y2_z2_6_materialType: - default: '' - type: string - WH4_x3_y2_z2_6_quantity: - default: 0.0 - type: number - WH4_x3_y2_z2_6_targetWH: - default: '' - type: string - WH4_x3_y3_z2_9_materialName: - default: '' - type: string - WH4_x3_y3_z2_9_materialType: - default: '' - type: string - WH4_x3_y3_z2_9_quantity: - default: 0.0 - type: number - WH4_x3_y3_z2_9_targetWH: - default: '' - type: string - WH4_x4_y1_z1_4_materialName: - default: '' - type: string - WH4_x4_y1_z1_4_quantity: - default: 0.0 - type: number - WH4_x4_y2_z1_9_materialName: - default: '' - type: string - WH4_x4_y2_z1_9_quantity: - default: 0.0 - type: number - WH4_x5_y1_z1_5_materialName: - default: '' - type: string - WH4_x5_y1_z1_5_quantity: - default: 0.0 - type: number - WH4_x5_y2_z1_10_materialName: - default: '' - type: string - WH4_x5_y2_z1_10_quantity: - default: 0.0 - type: number - xlsx_path: - default: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx + xlsx_path: + default: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx type: string required: [] type: object @@ -1368,451 +608,6 @@ bioyond_cell: title: scheduler_start_and_auto_feeding参数 type: object type: UniLabJsonCommand - auto-scheduler_start_and_auto_feeding_v2: - feedback: {} - goal: {} - goal_default: - WH3_x1_y1_z3_1_materialId: '' - WH3_x1_y1_z3_1_materialType: '' - WH3_x1_y1_z3_1_quantity: 0 - WH3_x1_y2_z3_4_materialId: '' - WH3_x1_y2_z3_4_materialType: '' - WH3_x1_y2_z3_4_quantity: 0 - WH3_x1_y3_z3_7_materialId: '' - WH3_x1_y3_z3_7_materialType: '' - WH3_x1_y3_z3_7_quantity: 0 - WH3_x1_y4_z3_10_materialId: '' - WH3_x1_y4_z3_10_materialType: '' - WH3_x1_y4_z3_10_quantity: 0 - WH3_x1_y5_z3_13_materialId: '' - WH3_x1_y5_z3_13_materialType: '' - WH3_x1_y5_z3_13_quantity: 0 - WH3_x2_y1_z3_2_materialId: '' - WH3_x2_y1_z3_2_materialType: '' - WH3_x2_y1_z3_2_quantity: 0 - WH3_x2_y2_z3_5_materialId: '' - WH3_x2_y2_z3_5_materialType: '' - WH3_x2_y2_z3_5_quantity: 0 - WH3_x2_y3_z3_8_materialId: '' - WH3_x2_y3_z3_8_materialType: '' - WH3_x2_y3_z3_8_quantity: 0 - WH3_x2_y4_z3_11_materialId: '' - WH3_x2_y4_z3_11_materialType: '' - WH3_x2_y4_z3_11_quantity: 0 - WH3_x2_y5_z3_14_materialId: '' - WH3_x2_y5_z3_14_materialType: '' - WH3_x2_y5_z3_14_quantity: 0 - WH3_x3_y1_z3_3_materialId: '' - WH3_x3_y1_z3_3_materialType: '' - WH3_x3_y1_z3_3_quantity: 0 - WH3_x3_y2_z3_6_materialId: '' - WH3_x3_y2_z3_6_materialType: '' - WH3_x3_y2_z3_6_quantity: 0 - WH3_x3_y3_z3_9_materialId: '' - WH3_x3_y3_z3_9_materialType: '' - WH3_x3_y3_z3_9_quantity: 0 - WH3_x3_y4_z3_12_materialId: '' - WH3_x3_y4_z3_12_materialType: '' - WH3_x3_y4_z3_12_quantity: 0 - WH3_x3_y5_z3_15_materialId: '' - WH3_x3_y5_z3_15_materialType: '' - WH3_x3_y5_z3_15_quantity: 0 - WH4_x1_y1_z1_1_materialName: '' - WH4_x1_y1_z1_1_quantity: 0.0 - WH4_x1_y1_z2_1_materialName: '' - WH4_x1_y1_z2_1_materialType: '' - WH4_x1_y1_z2_1_quantity: 0.0 - WH4_x1_y1_z2_1_targetWH: '' - WH4_x1_y2_z1_6_materialName: '' - WH4_x1_y2_z1_6_quantity: 0.0 - WH4_x1_y2_z2_4_materialName: '' - WH4_x1_y2_z2_4_materialType: '' - WH4_x1_y2_z2_4_quantity: 0.0 - WH4_x1_y2_z2_4_targetWH: '' - WH4_x1_y3_z1_11_materialName: '' - WH4_x1_y3_z1_11_quantity: 0.0 - WH4_x1_y3_z2_7_materialName: '' - WH4_x1_y3_z2_7_materialType: '' - WH4_x1_y3_z2_7_quantity: 0.0 - WH4_x1_y3_z2_7_targetWH: '' - WH4_x2_y1_z1_2_materialName: '' - WH4_x2_y1_z1_2_quantity: 0.0 - WH4_x2_y1_z2_2_materialName: '' - WH4_x2_y1_z2_2_materialType: '' - WH4_x2_y1_z2_2_quantity: 0.0 - WH4_x2_y1_z2_2_targetWH: '' - WH4_x2_y2_z1_7_materialName: '' - WH4_x2_y2_z1_7_quantity: 0.0 - WH4_x2_y2_z2_5_materialName: '' - WH4_x2_y2_z2_5_materialType: '' - WH4_x2_y2_z2_5_quantity: 0.0 - WH4_x2_y2_z2_5_targetWH: '' - WH4_x2_y3_z1_12_materialName: '' - WH4_x2_y3_z1_12_quantity: 0.0 - WH4_x2_y3_z2_8_materialName: '' - WH4_x2_y3_z2_8_materialType: '' - WH4_x2_y3_z2_8_quantity: 0.0 - WH4_x2_y3_z2_8_targetWH: '' - WH4_x3_y1_z1_3_materialName: '' - WH4_x3_y1_z1_3_quantity: 0.0 - WH4_x3_y1_z2_3_materialName: '' - WH4_x3_y1_z2_3_materialType: '' - WH4_x3_y1_z2_3_quantity: 0.0 - WH4_x3_y1_z2_3_targetWH: '' - WH4_x3_y2_z1_8_materialName: '' - WH4_x3_y2_z1_8_quantity: 0.0 - WH4_x3_y2_z2_6_materialName: '' - WH4_x3_y2_z2_6_materialType: '' - WH4_x3_y2_z2_6_quantity: 0.0 - WH4_x3_y2_z2_6_targetWH: '' - WH4_x3_y3_z2_9_materialName: '' - WH4_x3_y3_z2_9_materialType: '' - WH4_x3_y3_z2_9_quantity: 0.0 - WH4_x3_y3_z2_9_targetWH: '' - WH4_x4_y1_z1_4_materialName: '' - WH4_x4_y1_z1_4_quantity: 0.0 - WH4_x4_y2_z1_9_materialName: '' - WH4_x4_y2_z1_9_quantity: 0.0 - WH4_x5_y1_z1_5_materialName: '' - WH4_x5_y1_z1_5_quantity: 0.0 - WH4_x5_y2_z1_10_materialName: '' - WH4_x5_y2_z1_10_quantity: 0.0 - xlsx_path: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx - handles: {} - placeholder_keys: {} - result: {} - schema: - description: 组合函数V2版本(测试):先启动调度,然后执行自动化上料(使用非阻塞轮询等待) - properties: - feedback: {} - goal: - properties: - WH3_x1_y1_z3_1_materialId: - default: '' - type: string - WH3_x1_y1_z3_1_materialType: - default: '' - type: string - WH3_x1_y1_z3_1_quantity: - default: 0 - type: number - WH3_x1_y2_z3_4_materialId: - default: '' - type: string - WH3_x1_y2_z3_4_materialType: - default: '' - type: string - WH3_x1_y2_z3_4_quantity: - default: 0 - type: number - WH3_x1_y3_z3_7_materialId: - default: '' - type: string - WH3_x1_y3_z3_7_materialType: - default: '' - type: string - WH3_x1_y3_z3_7_quantity: - default: 0 - type: number - WH3_x1_y4_z3_10_materialId: - default: '' - type: string - WH3_x1_y4_z3_10_materialType: - default: '' - type: string - WH3_x1_y4_z3_10_quantity: - default: 0 - type: number - WH3_x1_y5_z3_13_materialId: - default: '' - type: string - WH3_x1_y5_z3_13_materialType: - default: '' - type: string - WH3_x1_y5_z3_13_quantity: - default: 0 - type: number - WH3_x2_y1_z3_2_materialId: - default: '' - type: string - WH3_x2_y1_z3_2_materialType: - default: '' - type: string - WH3_x2_y1_z3_2_quantity: - default: 0 - type: number - WH3_x2_y2_z3_5_materialId: - default: '' - type: string - WH3_x2_y2_z3_5_materialType: - default: '' - type: string - WH3_x2_y2_z3_5_quantity: - default: 0 - type: number - WH3_x2_y3_z3_8_materialId: - default: '' - type: string - WH3_x2_y3_z3_8_materialType: - default: '' - type: string - WH3_x2_y3_z3_8_quantity: - default: 0 - type: number - WH3_x2_y4_z3_11_materialId: - default: '' - type: string - WH3_x2_y4_z3_11_materialType: - default: '' - type: string - WH3_x2_y4_z3_11_quantity: - default: 0 - type: number - WH3_x2_y5_z3_14_materialId: - default: '' - type: string - WH3_x2_y5_z3_14_materialType: - default: '' - type: string - WH3_x2_y5_z3_14_quantity: - default: 0 - type: number - WH3_x3_y1_z3_3_materialId: - default: '' - type: string - WH3_x3_y1_z3_3_materialType: - default: '' - type: string - WH3_x3_y1_z3_3_quantity: - default: 0 - type: number - WH3_x3_y2_z3_6_materialId: - default: '' - type: string - WH3_x3_y2_z3_6_materialType: - default: '' - type: string - WH3_x3_y2_z3_6_quantity: - default: 0 - type: number - WH3_x3_y3_z3_9_materialId: - default: '' - type: string - WH3_x3_y3_z3_9_materialType: - default: '' - type: string - WH3_x3_y3_z3_9_quantity: - default: 0 - type: number - WH3_x3_y4_z3_12_materialId: - default: '' - type: string - WH3_x3_y4_z3_12_materialType: - default: '' - type: string - WH3_x3_y4_z3_12_quantity: - default: 0 - type: number - WH3_x3_y5_z3_15_materialId: - default: '' - type: string - WH3_x3_y5_z3_15_materialType: - default: '' - type: string - WH3_x3_y5_z3_15_quantity: - default: 0 - type: number - WH4_x1_y1_z1_1_materialName: - default: '' - type: string - WH4_x1_y1_z1_1_quantity: - default: 0.0 - type: number - WH4_x1_y1_z2_1_materialName: - default: '' - type: string - WH4_x1_y1_z2_1_materialType: - default: '' - type: string - WH4_x1_y1_z2_1_quantity: - default: 0.0 - type: number - WH4_x1_y1_z2_1_targetWH: - default: '' - type: string - WH4_x1_y2_z1_6_materialName: - default: '' - type: string - WH4_x1_y2_z1_6_quantity: - default: 0.0 - type: number - WH4_x1_y2_z2_4_materialName: - default: '' - type: string - WH4_x1_y2_z2_4_materialType: - default: '' - type: string - WH4_x1_y2_z2_4_quantity: - default: 0.0 - type: number - WH4_x1_y2_z2_4_targetWH: - default: '' - type: string - WH4_x1_y3_z1_11_materialName: - default: '' - type: string - WH4_x1_y3_z1_11_quantity: - default: 0.0 - type: number - WH4_x1_y3_z2_7_materialName: - default: '' - type: string - WH4_x1_y3_z2_7_materialType: - default: '' - type: string - WH4_x1_y3_z2_7_quantity: - default: 0.0 - type: number - WH4_x1_y3_z2_7_targetWH: - default: '' - type: string - WH4_x2_y1_z1_2_materialName: - default: '' - type: string - WH4_x2_y1_z1_2_quantity: - default: 0.0 - type: number - WH4_x2_y1_z2_2_materialName: - default: '' - type: string - WH4_x2_y1_z2_2_materialType: - default: '' - type: string - WH4_x2_y1_z2_2_quantity: - default: 0.0 - type: number - WH4_x2_y1_z2_2_targetWH: - default: '' - type: string - WH4_x2_y2_z1_7_materialName: - default: '' - type: string - WH4_x2_y2_z1_7_quantity: - default: 0.0 - type: number - WH4_x2_y2_z2_5_materialName: - default: '' - type: string - WH4_x2_y2_z2_5_materialType: - default: '' - type: string - WH4_x2_y2_z2_5_quantity: - default: 0.0 - type: number - WH4_x2_y2_z2_5_targetWH: - default: '' - type: string - WH4_x2_y3_z1_12_materialName: - default: '' - type: string - WH4_x2_y3_z1_12_quantity: - default: 0.0 - type: number - WH4_x2_y3_z2_8_materialName: - default: '' - type: string - WH4_x2_y3_z2_8_materialType: - default: '' - type: string - WH4_x2_y3_z2_8_quantity: - default: 0.0 - type: number - WH4_x2_y3_z2_8_targetWH: - default: '' - type: string - WH4_x3_y1_z1_3_materialName: - default: '' - type: string - WH4_x3_y1_z1_3_quantity: - default: 0.0 - type: number - WH4_x3_y1_z2_3_materialName: - default: '' - type: string - WH4_x3_y1_z2_3_materialType: - default: '' - type: string - WH4_x3_y1_z2_3_quantity: - default: 0.0 - type: number - WH4_x3_y1_z2_3_targetWH: - default: '' - type: string - WH4_x3_y2_z1_8_materialName: - default: '' - type: string - WH4_x3_y2_z1_8_quantity: - default: 0.0 - type: number - WH4_x3_y2_z2_6_materialName: - default: '' - type: string - WH4_x3_y2_z2_6_materialType: - default: '' - type: string - WH4_x3_y2_z2_6_quantity: - default: 0.0 - type: number - WH4_x3_y2_z2_6_targetWH: - default: '' - type: string - WH4_x3_y3_z2_9_materialName: - default: '' - type: string - WH4_x3_y3_z2_9_materialType: - default: '' - type: string - WH4_x3_y3_z2_9_quantity: - default: 0.0 - type: number - WH4_x3_y3_z2_9_targetWH: - default: '' - type: string - WH4_x4_y1_z1_4_materialName: - default: '' - type: string - WH4_x4_y1_z1_4_quantity: - default: 0.0 - type: number - WH4_x4_y2_z1_9_materialName: - default: '' - type: string - WH4_x4_y2_z1_9_quantity: - default: 0.0 - type: number - WH4_x5_y1_z1_5_materialName: - default: '' - type: string - WH4_x5_y1_z1_5_quantity: - default: 0.0 - type: number - WH4_x5_y2_z1_10_materialName: - default: '' - type: string - WH4_x5_y2_z1_10_quantity: - default: 0.0 - type: number - xlsx_path: - default: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx - type: string - required: [] - type: object - result: {} - required: - - goal - title: scheduler_start_and_auto_feeding_v2参数 - type: object - type: UniLabJsonCommand auto-scheduler_stop: feedback: {} goal: {} @@ -1953,6 +748,7 @@ bioyond_cell: type: object type: UniLabJsonCommand auto-transfer_3_to_2_to_1: + always_free: true feedback: {} goal: {} goal_default: @@ -1989,6 +785,126 @@ bioyond_cell: title: transfer_3_to_2_to_1参数 type: object type: UniLabJsonCommand + auto-transfer_3_to_2_to_1_auto: + feedback: {} + goal: {} + goal_default: + target_device: BatteryStation + target_location: bottle_rack_6x2 + vial_plates: null + handles: + input: + - data_key: '@this@@@vial_plates' + data_source: handle + data_type: array + handler_key: vial_plates_input + label: 分液瓶板列表 + output: + - data_key: total + data_source: executor + data_type: integer + handler_key: transfer_total + label: 转运总数 + - data_key: success + data_source: executor + data_type: integer + handler_key: transfer_success + label: 成功数量 + - data_key: failed + data_source: executor + data_type: integer + handler_key: transfer_failed + label: 失败数量 + placeholder_keys: {} + result: + properties: + failed: + type: integer + results: + items: + properties: + error: + type: string + index: + type: integer + materialId: + type: string + orderCode: + type: string + result: + type: object + status: + type: string + type: object + type: array + success: + type: integer + total: + type: integer + type: object + schema: + description: 自动批量转运分液瓶板(从配液站到扣电站) + properties: + feedback: {} + goal: + properties: + target_device: + default: coin_cell_assembly + description: 目标设备ID + type: string + target_location: + default: bottle_rack_6x2 + description: 目标资源名称 + type: string + vial_plates: + description: 分液瓶板列表(从create_orders的vial_plates获取) + items: + properties: + locationId: + type: string + materialId: + type: string + orderCode: + type: string + required: + - locationId + - materialId + type: object + type: array + required: + - vial_plates + type: object + result: + properties: + failed: + type: integer + results: + items: + properties: + error: + type: string + index: + type: integer + materialId: + type: string + orderCode: + type: string + result: + type: object + status: + type: string + type: object + type: array + success: + type: integer + total: + type: integer + type: object + required: + - goal + title: transfer_3_to_2_to_1_auto参数 + type: object + type: UniLabJsonCommand auto-update_push_ip: feedback: {} goal: {} @@ -2113,7 +1029,6 @@ bioyond_cell: module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation status_types: device_id: String - material_info: dict type: python config_info: [] description: '' @@ -2134,11 +1049,8 @@ bioyond_cell: properties: device_id: type: string - material_info: - type: object required: - device_id - - material_info type: object registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml index 2e9f60739..5bdf56d58 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -70,51 +70,6 @@ coincellassemblyworkstation_device: 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: {} - required: - - goal - title: func_allpack_cmd参数 - type: object - type: UniLabJsonCommand auto-func_allpack_cmd_simp: feedback: {} goal: {} @@ -390,12 +345,10 @@ coincellassemblyworkstation_device: handles: input: - data_key: bottle_num - data_source: workflow + data_source: handle data_type: integer handler_key: bottle_count - io_type: source label: 配液瓶数 - required: true placeholder_keys: {} result: {} schema: @@ -523,12 +476,15 @@ coincellassemblyworkstation_device: handles: input: - data_key: elec_num - data_source: workflow + data_source: handle data_type: integer handler_key: bottle_count - io_type: source label: 配液瓶数 - required: true + - data_key: formulations + data_source: handle + data_type: array + handler_key: formulations_input + label: 配方信息列表 placeholder_keys: {} result: {} schema: @@ -743,6 +699,10 @@ 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 @@ -750,14 +710,22 @@ coincellassemblyworkstation_device: data_axis_y_pos: float data_axis_z_pos: float data_coin_cell_code: str - data_coin_num: int + data_coin_type: int + data_current_assembling_count: int + data_current_completed_count: 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 @@ -787,6 +755,14 @@ 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: @@ -801,22 +777,38 @@ coincellassemblyworkstation_device: type: number data_coin_cell_code: type: string - data_coin_num: + data_coin_type: + type: integer + data_current_assembling_count: + type: integer + data_current_completed_count: 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: @@ -831,7 +823,6 @@ coincellassemblyworkstation_device: - request_rec_msg_status - request_send_msg_status - data_assembly_coin_cell_num - - data_assembly_time - data_open_circuit_voltage - data_axis_x_pos - data_axis_y_pos @@ -839,12 +830,24 @@ coincellassemblyworkstation_device: - data_pole_weight - data_assembly_pressure - data_electrolyte_volume - - data_coin_num + - data_coin_type + - data_current_assembling_count + - data_current_completed_count - data_coin_cell_code - data_electrolyte_code - data_glove_box_pressure - data_glove_box_o2_content - 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 type: object registry_type: device version: 1.0.0 diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 2a277664a..d6b9ea071 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -632,6 +632,11 @@ def _preserve_field_descriptions(self, new_schema: Dict[str, Any], previous_sche # 保留字段的 title(用户自定义的中文名) if "title" in prev_field and prev_field["title"]: field_schema["title"] = prev_field["title"] + # 保留旧 schema 中手动定义的复杂嵌套结构(如 items、properties、required) + # 当旧 schema 比自动生成的更丰富时,使用旧 schema 的结构 + for rich_key in ("items", "properties", "required"): + if rich_key in prev_field and rich_key not in field_schema: + field_schema[rich_key] = prev_field[rich_key] def _is_typed_dict(self, annotation: Any) -> bool: """ @@ -818,20 +823,26 @@ def _load_single_device_file( "goal_default": {i["name"]: i["default"] for i in v["args"]}, "handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []), "placeholder_keys": { - i["name"]: ( - "unilabos_resources" - if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" - or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot") - else "unilabos_devices" - ) - for i in v["args"] - if i.get("type", "") - in [ - "unilabos.registry.placeholder_type:ResourceSlot", - "unilabos.registry.placeholder_type:DeviceSlot", - ("list", "unilabos.registry.placeholder_type:ResourceSlot"), - ("list", "unilabos.registry.placeholder_type:DeviceSlot"), - ] + # 先用旧配置中手动定义的 placeholder_keys 作为基础 + **old_action_configs.get(f"auto-{k}", {}).get("placeholder_keys", {}), + # 再用自动推断的覆盖(ResourceSlot/DeviceSlot 类型) + **{ + i["name"]: ( + "unilabos_resources" + if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" + or i["type"] + == ("list", "unilabos.registry.placeholder_type:ResourceSlot") + else "unilabos_devices" + ) + for i in v["args"] + if i.get("type", "") + in [ + "unilabos.registry.placeholder_type:ResourceSlot", + "unilabos.registry.placeholder_type:DeviceSlot", + ("list", "unilabos.registry.placeholder_type:ResourceSlot"), + ("list", "unilabos.registry.placeholder_type:DeviceSlot"), + ] + }, }, **({"always_free": True} if v.get("always_free") else {}), } diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index 8d6993b17..3408a0a9f 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -22,11 +22,11 @@ BIOYOND_PolymerReactionStation_Deck: init_param_schema: {} registry_type: resource version: 1.0.0 -BIOYOND_YB_Deck: +BioyondElectrolyteDeck: category: - deck class: - module: unilabos.resources.bioyond.decks:YB_Deck + module: unilabos.resources.bioyond.decks:bioyond_electrolyte_deck type: pylabrobot description: BIOYOND ElectrolyteFormulationStation Deck handles: [] @@ -34,11 +34,11 @@ BIOYOND_YB_Deck: init_param_schema: {} registry_type: resource version: 1.0.0 -CoincellDeck: +YihuaCoinCellDeck: category: - deck class: - module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:YH_Deck + module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:yihua_coin_cell_deck type: pylabrobot description: YIHUA CoinCellAssembly Deck handles: [] diff --git a/unilabos/resources/battery/bottle_carriers.py b/unilabos/resources/battery/bottle_carriers.py index 9d9827cdd..4003ae73b 100644 --- a/unilabos/resources/battery/bottle_carriers.py +++ b/unilabos/resources/battery/bottle_carriers.py @@ -1,9 +1,6 @@ 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 @@ -51,6 +48,5 @@ def YIHUA_Electrolyte_12VialCarrier(name: str) -> BottleCarrier: carrier.num_items_x = 2 carrier.num_items_y = 6 carrier.num_items_z = 1 - for i in range(12): - carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}") + # 载架初始化为空,瓶子由实际转运操作填入,避免反序列化时重复 assign return carrier diff --git a/unilabos/resources/battery/magazine.py b/unilabos/resources/battery/magazine.py index 04328a407..aeddea7b4 100644 --- a/unilabos/resources/battery/magazine.py +++ b/unilabos/resources/battery/magazine.py @@ -53,13 +53,28 @@ def size_z(self) -> float: return self.get_size_z() def serialize(self) -> dict: - return { - **super().serialize(), + data = super().serialize() + # 物料余量由寄存器接管,不再持久化极片子节点, + # 防止旧数据写回数据库后下次启动时再次引发重复 UUID。 + data["children"] = [] + data.update({ "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): @@ -220,7 +235,7 @@ def MagazineHolder_6_Cathode( size_y=size_y, size_z=size_z, locations=locations, - klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan], + klasses=None, hole_diameter=hole_diameter, hole_depth=hole_depth, max_sheets_per_hole=max_sheets_per_hole, @@ -258,7 +273,7 @@ def MagazineHolder_6_Anode( size_y=size_y, size_z=size_z, locations=locations, - klasses=[SpringWasher, NegativeCan, NegativeCan, SpringWasher, NegativeCan, NegativeCan], + klasses=None, hole_diameter=hole_diameter, hole_depth=hole_depth, max_sheets_per_hole=max_sheets_per_hole, @@ -335,7 +350,7 @@ def MagazineHolder_4_Cathode( size_y=size_y, size_z=size_z, locations=locations, - klasses=[AluminumFoil, PositiveElectrode, PositiveElectrode, PositiveElectrode], + klasses=None, hole_diameter=hole_diameter, hole_depth=hole_depth, max_sheets_per_hole=max_sheets_per_hole, diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index 5f3b2c4ec..fdc470e4b 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -1,4 +1,3 @@ -from os import name from pylabrobot.resources import Deck, Coordinate, Rotation from unilabos.resources.bioyond.YB_warehouses import ( @@ -34,11 +33,8 @@ def __init__( 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: # 添加仓库 @@ -66,6 +62,7 @@ def setup(self) -> None: 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, @@ -74,11 +71,8 @@ def __init__( 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系统中的实际名称 @@ -101,7 +95,8 @@ def setup(self) -> None: for warehouse_name, warehouse in self.warehouses.items(): self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) -class BIOYOND_YB_Deck(Deck): + +class BioyondElectrolyteDeck(Deck): def __init__( self, name: str = "YB_Deck", @@ -109,17 +104,14 @@ def __init__( size_y: float = 1400.0, size_z: float = 2670.0, category: str = "deck", - setup: bool = False ) -> None: super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0) - if setup: - self.setup() def setup(self) -> None: # 添加仓库 self.warehouses = { - "321窗口": bioyond_warehouse_2x2x1("321窗口"), # 2行×2列 - "43窗口": bioyond_warehouse_2x2x1("43窗口"), # 2行×2列 + "自动堆栈-左": bioyond_warehouse_2x2x1("自动堆栈-左"), # 2行×2列 + "自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"), # 2行×2列 "手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03 "手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03 "加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"), @@ -133,29 +125,34 @@ def setup(self) -> None: } # warehouse 的位置 self.warehouse_locations = { - "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, 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), # 新增:位置需根据实际硬件调整 + "自动堆栈-左": 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), + + "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), # 新增:位置需根据实际硬件调整 } for warehouse_name, warehouse in self.warehouses.items(): self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) -def YB_Deck(name: str) -> Deck: - by=BIOYOND_YB_Deck(name=name) - by.setup() - return by +# 向后兼容别名,日后废弃 +BIOYOND_YB_Deck = BioyondElectrolyteDeck +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) diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index fe55c39e5..04875fa4f 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -179,6 +179,35 @@ def assign_child_resource( 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] diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index b34d10cc0..5c7a2c4e6 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -585,6 +585,31 @@ def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): d["model"] = res.config.get("model", None) return d + def _deduplicate_plr_dict(d: dict, _seen: set = None) -> dict: + """递归去除 children 中同名重复节点(全树范围、保留首次出现)。 + + 根本原因:同一槽位被 sync_from_external(Bioyond 同步)重复写入, + 导致数据库中同一 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() @@ -595,6 +620,8 @@ def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): 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": @@ -613,6 +640,14 @@ def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): 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) diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py index 929a4e4de..865fa65d2 100644 --- a/unilabos/resources/warehouse.py +++ b/unilabos/resources/warehouse.py @@ -41,8 +41,9 @@ def warehouse_factory( # 根据 layout 决定 y 坐标计算 if layout == "row-major": - # 行优先:row=0(A行) 应该显示在上方,需要较小的 y 值 - y = dy + row * item_dy + # 行优先:row=0(A行) 应该显示在上方 + # 前端现在 y 越大越靠上,所以 row=0 对应最大的 y + y = dy + (num_items_y - row - 1) * item_dy elif layout == "vertical-col-major": # 竖向warehouse: row=0 对应顶部(y小),row=n-1 对应底部(y大) # 但标签 01 应该在底部,所以使用反向映射 diff --git a/unilabos/test/experiments/yibin_electrolyte_config_example.json b/unilabos/test/experiments/yibin_electrolyte_config_example.json index d5efc3578..ba25c0ac9 100644 --- a/unilabos/test/experiments/yibin_electrolyte_config_example.json +++ b/unilabos/test/experiments/yibin_electrolyte_config_example.json @@ -13,7 +13,7 @@ "deck": { "data": { "_resource_child_name": "YB_Bioyond_Deck", - "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck" + "_resource_type": "unilabos.resources.bioyond.decks:BioyondElectrolyteDeck" } }, "protocol_type": [], @@ -103,15 +103,14 @@ "children": [], "parent": "bioyond_cell_workstation", "type": "deck", - "class": "BIOYOND_YB_Deck", + "class": "BioyondElectrolyteDeck", "position": { "x": 0, "y": 0, "z": 0 }, "config": { - "type": "BIOYOND_YB_Deck", - "setup": true, + "type": "BioyondElectrolyteDeck", "rotation": { "x": 0, "y": 0, diff --git a/unilabos/utils/log.py b/unilabos/utils/log.py index be5d8c312..cee3269b2 100644 --- a/unilabos/utils/log.py +++ b/unilabos/utils/log.py @@ -193,7 +193,6 @@ def configure_logger(loglevel=None, working_dir=None): root_logger.addHandler(console_handler) # 如果指定了工作目录,添加文件处理器 - log_filepath = None if working_dir is not None: logs_dir = os.path.join(working_dir, "logs") os.makedirs(logs_dir, exist_ok=True) @@ -214,7 +213,6 @@ def configure_logger(loglevel=None, working_dir=None): logging.getLogger("asyncio").setLevel(logging.INFO) logging.getLogger("urllib3").setLevel(logging.INFO) - return log_filepath