From 73add2dc065b538f9f6046134147071f89ad5172 Mon Sep 17 00:00:00 2001 From: Andy6M Date: Wed, 15 Apr 2026 12:07:01 +0800 Subject: [PATCH] feat: implement electrolyte CSV export and barcode tracking - add CSV export for order data in bioyond_cell - extract prep and vial bottles from order_finish report - update bioyond_cell registry with csv_export_path - update coin_cell_assembly to export new bottle barcodes and mass ratios - add 260415csv_export_walkthrough.md --- 260415csv_export_walkthrough.md | 72 ++++ .../bioyond_cell/bioyond_cell_workstation.py | 390 +++++++++++++++++- .../coin_cell_assembly/coin_cell_assembly.py | 25 +- unilabos/registry/devices/bioyond_cell.yaml | 17 + 4 files changed, 490 insertions(+), 14 deletions(-) create mode 100644 260415csv_export_walkthrough.md diff --git a/260415csv_export_walkthrough.md b/260415csv_export_walkthrough.md new file mode 100644 index 00000000..b783c852 --- /dev/null +++ b/260415csv_export_walkthrough.md @@ -0,0 +1,72 @@ +# CSV 导出功能变更概要 + +## 修改的文件 + +### 1. [bioyond_cell_workstation.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py) + +#### 新增导入 +- `import csv` 和 `import os`(L14-15) + +#### 新增方法 + +| 方法 | 功能 | +|------|------| +| `_extract_prep_bottle_from_report` | 从 order_finish 报文提取**配液瓶**信息(每订单最多1个) | +| `_extract_vial_bottles_from_report` | 从 order_finish 报文提取**分液瓶**信息(每订单可多个,返回数组) | +| `_export_order_csv` | 汇总所有信息写入 CSV 文件 | + +#### 配液瓶筛选逻辑 (`_extract_prep_bottle_from_report`) +- `typemode="1"`, `realQuantity=1`, `usedQuantity=1` +- `locationId` 以 `3a19deae-2c7a-` 开头(手动传递窗) +- LIMS API 二次确认:`typeName` 含"配液瓶(小)"或"配液瓶(大)" + +#### 分液瓶筛选逻辑 (`_extract_vial_bottles_from_report`) +- `typemode="1"`, `realQuantity=1`, `usedQuantity=1` +- `locationId` 以 `3a19debc-84b5-` 或 `3a19debe-5200` 开头(自动堆栈-左/右) +- LIMS API 二次确认:`typeName` 为"5ml分液瓶"或"20ml分液瓶" +- **返回数组**,支持 1×5ml + n×20ml 的组合 + +#### 修改的方法 + +| 方法 | 变更 | +|------|------| +| `_submit_and_wait_orders` | 新增配液瓶+分液瓶提取步骤,将 `prep_bottles` 和 `vial_bottles` 存入 `final_result` | +| `create_orders` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` | +| `create_orders_formulation` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` | + +#### CSV 输出格式 +``` +orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间 +``` +- 单个分液瓶时直接写值;多个分液瓶时类型和二维码用 JSON 数组表示 +- CSV 编码使用 `utf-8-sig`(兼容 Excel 打开) +- `csv_export_path` 默认为空字符串,不传则不导出(向后兼容) + +--- + +### 2. [bioyond_cell.yaml](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/registry/devices/bioyond_cell.yaml) + +为两个 action 注册了 `csv_export_path` 参数: + +- `auto-create_orders`: `goal_default` + `schema.properties.goal.properties` 中添加 `csv_export_path` +- `auto-create_orders_formulation`: 同上 + +--- + +### 3. [coin_cell_assembly.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py) 的 CSV 改动与全流程追溯 + +在 `bioyond_cell_workstation.py` 的 `_submit_and_wait_orders` 最后阶段,提取 `prep_bottles`(配液瓶)和 `vial_bottles`(分液瓶)的条码并随 `mass_ratios` 数组一起下发给各下游工站(例如扣电组装站),实现跨站的全流程配方追溯。 + +并在扣电站生成的 `date_xxx.csv` 中,**替换并新增**了以下列: +- 移除了原有的 `formulation_order_code` 与合并的 `formulation_ratio` 列。 +- 新增 `orderName` 导出 +- 新增 `prep_bottle_barcode`(奔曜传递的配液瓶二维码) +- 新增 `vial_bottle_barcodes`(奔曜传递的分液瓶二维码,多瓶时存 JSON 数组) +- 新增 `target_mass_ratio` 理论目标质量比 +- 新增 `real_mass_ratio` 实际称量真实质量比 + +*注意:这与操作人员在手套箱内扫码传入扣电站的 `electrolyte_code` 是单独记录的,方便做数据核对。* + +## 向后兼容性 +- `csv_export_path` 默认值为 `""`(空字符串),现有调用不受影响 +- 新增的 `prep_bottles` 和 `vial_bottles` 字段为 `final_result` 和 `mass_ratios` 内部的新增附属字段,不破坏现有数据结构。 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 a88c1b3f..ce199b2b 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 @@ -11,6 +11,8 @@ from datetime import datetime, timedelta import re import threading import json +import csv +import os from copy import deepcopy from urllib3 import response from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation, BioyondResourceSynchronizer @@ -803,6 +805,49 @@ class BioyondCellWorkstation(BioyondWorkstation): f"(对应 {len(processed_material_ids)} 个物理瓶板)" ) + # ========== 提取配液瓶 + 分液瓶信息(用于 CSV 导出)========== + all_prep_bottles = [] + all_vial_bottles = [] + for report in all_reports: + # 提取配液瓶(每个订单最多一个) + try: + prep_info = self._extract_prep_bottle_from_report(report) + all_prep_bottles.append(prep_info) + except Exception as e: + logger.error(f"[提取配液瓶] 异常: orderCode={report.get('orderCode')}, 错误={e}") + all_prep_bottles.append(None) + + # 提取分液瓶(每个订单可能多个) + try: + vial_list = self._extract_vial_bottles_from_report(report) + all_vial_bottles.append(vial_list) + except Exception as e: + logger.error(f"[提取分液瓶] 异常: orderCode={report.get('orderCode')}, 错误={e}") + all_vial_bottles.append([]) + + logger.info( + f"[{tag}] 配液瓶提取完成: {sum(1 for p in all_prep_bottles if p)} 个, " + f"分液瓶提取完成: {sum(len(v) for v in all_vial_bottles if isinstance(v, list))} 个" + ) + + # ========== 将条码附加到 mass_ratios 中(给扣电组装站使用)========== + for idx in range(len(all_mass_ratios)): + if idx < len(all_prep_bottles) and all_prep_bottles[idx]: + all_mass_ratios[idx]["prep_bottle_barcode"] = all_prep_bottles[idx].get("barCode", "") + else: + all_mass_ratios[idx]["prep_bottle_barcode"] = "" + + if idx < len(all_vial_bottles): + vials = all_vial_bottles[idx] + if len(vials) == 0: + all_mass_ratios[idx]["vial_bottle_barcodes"] = "" + elif len(vials) == 1: + all_mass_ratios[idx]["vial_bottle_barcodes"] = vials[0].get("barCode", "") + else: + all_mass_ratios[idx]["vial_bottle_barcodes"] = json.dumps([v.get("barCode", "") for v in vials], ensure_ascii=False) + else: + all_mass_ratios[idx]["vial_bottle_barcodes"] = "" + # ========== 构造最终结果 ========== final_result = { "status": "all_completed", @@ -811,6 +856,8 @@ class BioyondCellWorkstation(BioyondWorkstation): "reports": all_reports, "mass_ratios": all_mass_ratios, "vial_plates": all_vial_plates, + "prep_bottles": all_prep_bottles, + "vial_bottles": all_vial_bottles, "original_response": response, } @@ -828,7 +875,7 @@ class BioyondCellWorkstation(BioyondWorkstation): return final_result # -------------------- 2.14 新建实验(Excel 入口) -------------------- - def create_orders(self, xlsx_path: str) -> Dict[str, Any]: + def create_orders(self, xlsx_path: str, csv_export_path: str = "") -> Dict[str, Any]: """ 从 Excel 解析并创建实验(2.14)- V2版本 约定: @@ -972,18 +1019,30 @@ class BioyondCellWorkstation(BioyondWorkstation): logger.error("[create_orders] 没有有效的订单可提交") return {"status": "error", "message": "没有有效订单数据"} - return self._submit_and_wait_orders(orders, tag="create_orders") + result = self._submit_and_wait_orders(orders, tag="create_orders") + + # ========== CSV 导出 ========== + if csv_export_path: + try: + csv_file = self._export_order_csv(result, csv_export_path) + result["csv_file"] = csv_file + except Exception as e: + logger.error(f"[create_orders] CSV 导出失败: {e}") + + return result def create_orders_formulation( self, formulation: List[Dict[str, Any]], batch_id: str = "", + order_names: List[str] = [], bottle_type: str = "配液小瓶", mix_time: List[int] = [], coin_cell_volume: float = 0.0, pouch_cell_volume: float = 0.0, conductivity_volume: float = 0.0, conductivity_bottle_count: int = 0, + csv_export_path: str = "", ) -> Dict[str, Any]: """ 配方批量输入版本的 create_orders —— 等价于 create_orders, @@ -1002,6 +1061,9 @@ class BioyondCellWorkstation(BioyondWorkstation): ... ] batch_id: 批次ID,若为空则用当前时间戳 + order_names: 配方ID/订单编号列表,与 formulation 一一对应。 + 用于填写 DoE 撒点编号等自定义标识,便于后续扣电组装、测试环节追溯。 + 优先级:order_names > formulation 内的 order_name > 自动生成({batch_id}_order_{序号}) bottle_type: 配液瓶类型,默认 "配液小瓶" mix_time: 混匀时间列表(秒),与 formulation 一一对应,不足则补 0 coin_cell_volume: 纽扣电池组装分液体积 @@ -1024,7 +1086,10 @@ class BioyondCellWorkstation(BioyondWorkstation): 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}") + if idx < len(order_names) and order_names[idx]: + order_name = order_names[idx] + else: + order_name = item.get("order_name", f"{batch_id}_order_{idx + 1}") mats: List[Dict[str, Any]] = [] total_mass = 0.0 @@ -1067,7 +1132,17 @@ class BioyondCellWorkstation(BioyondWorkstation): logger.error("[create_orders_formulation] 没有有效的订单可提交") return {"status": "error", "message": "没有有效配方数据"} - return self._submit_and_wait_orders(orders, tag="create_orders_formulation") + result = self._submit_and_wait_orders(orders, tag="create_orders_formulation") + + # ========== CSV 导出 ========== + if csv_export_path: + try: + csv_file = self._export_order_csv(result, csv_export_path) + result["csv_file"] = csv_file + except Exception as e: + logger.error(f"[create_orders_formulation] CSV 导出失败: {e}") + + return result def _extract_vial_plate_from_report(self, report: Dict) -> Optional[Dict]: """ @@ -1154,7 +1229,312 @@ class BioyondCellWorkstation(BioyondWorkstation): logger.warning(f"[提取分液瓶板] ❌ 未找到分液瓶板: orderCode={order_code}") return None - + + def _extract_prep_bottle_from_report(self, report: Dict) -> Optional[Dict]: + """ + 从 order_finish 报文中提取配液瓶信息 + + 筛选条件: + - typemode == "1" 且 realQuantity == 1 且 usedQuantity == 1 + - locationId 以 "3a19deae-2c7a-" 开头(手动传递窗右或手动传递窗左) + 二次确认: + - 调用 LIMS API 2.4,typeName 包含 "配液瓶(小)" 或 "配液瓶(大)" + + Args: + report: LIMS order_finish 报文 + + Returns: + { + "materialId": "...", + "locationId": "...", + "orderCode": "...", + "typeName": "配液瓶(小)" or "配液瓶(大)", + "barCode": "..." + } + 未找到时返回 None + """ + order_code = report.get("orderCode", "N/A") + used_materials = report.get("usedMaterials", []) + + logger.info( + f"[提取配液瓶] 开始处理订单 orderCode={order_code}, " + f"物料数量={len(used_materials)}" + ) + + # 手动传递窗右/左的 locationId 前缀 + MANUAL_WINDOW_PREFIX = "3a19deae-2c7a-" + + for idx, material in enumerate(used_materials): + location_id = material.get("locationId", "") + typemode = material.get("typemode", "") + material_id = material.get("materialId", "") + real_qty = material.get("realQuantity") + used_qty = material.get("usedQuantity") + + # 筛选条件:typemode=1, realQuantity=1, usedQuantity=1, 手动传递窗位置 + if ( + typemode == "1" + and real_qty == 1 + and used_qty == 1 + and location_id + and location_id.startswith(MANUAL_WINDOW_PREFIX) + ): + logger.debug( + f"[提取配液瓶] 候选物料 #{idx+1}: materialId={material_id[:20]}..." + ) + + # 调用 LIMS API 2.4 确认类型 + try: + material_info = self._query_material_info(material_id) + type_name = material_info.get("typeName", "") + + if "配液瓶(小)" in type_name or "配液瓶(大)" in type_name: + logger.info( + f"[提取配液瓶] ✅ 确认为配液瓶: orderCode={order_code}, " + f"typeName={type_name}, barCode={material_info.get('barCode')}" + ) + return { + "materialId": material_id, + "locationId": location_id, + "orderCode": order_code, + "typeName": type_name, + "barCode": material_info.get("barCode"), + } + else: + logger.debug( + f"[提取配液瓶] 候选物料不是配液瓶: typeName={type_name}, 跳过" + ) + except Exception as e: + logger.warning( + f"[提取配液瓶] ⚠️ 查询物料详情失败: materialId={material_id}, 错误={e}" + ) + + logger.warning(f"[提取配液瓶] ❌ 未找到配液瓶: orderCode={order_code}") + return None + + def _extract_vial_bottles_from_report(self, report: Dict) -> List[Dict]: + """ + 从 order_finish 报文中提取分液瓶信息(注意不是分液瓶板) + + 一个 orderCode 可能对应多个分液瓶: + - 1 × 5ml分液瓶 + - n × 20ml分液瓶 (n=1~4) + - 1 × 5ml分液瓶 + n × 20ml分液瓶 (n=1~4) + + 筛选条件: + - typemode == "1" 且 realQuantity == 1 且 usedQuantity == 1 + - locationId 以 "3a19debc-84b5-" 或 "3a19debe-5200" 开头 + (自动堆栈-左 或 自动堆栈-右) + 二次确认: + - typeName 为 "5ml分液瓶" 或 "20ml分液瓶" + + Args: + report: LIMS order_finish 报文 + + Returns: + 分液瓶信息列表,每个元素: + { + "materialId": "...", + "locationId": "...", + "orderCode": "...", + "typeName": "5ml分液瓶" or "20ml分液瓶", + "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_PREFIXES = ("3a19debc-84b5-", "3a19debe-5200") + + vial_bottles: List[Dict] = [] + + for idx, material in enumerate(used_materials): + location_id = material.get("locationId", "") + typemode = material.get("typemode", "") + material_id = material.get("materialId", "") + real_qty = material.get("realQuantity") + used_qty = material.get("usedQuantity") + + # 筛选条件 + if ( + typemode == "1" + and real_qty == 1 + and used_qty == 1 + and location_id + and any(location_id.startswith(p) for p in AUTO_STACK_PREFIXES) + ): + logger.debug( + f"[提取分液瓶] 候选物料 #{idx+1}: materialId={material_id[:20]}..." + ) + + # 调用 LIMS API 2.4 确认类型 + try: + material_info = self._query_material_info(material_id) + type_name = material_info.get("typeName", "") + + if type_name in ("5ml分液瓶", "20ml分液瓶"): + bar_code = material_info.get("barCode") + logger.info( + f"[提取分液瓶] ✅ 确认为分液瓶: orderCode={order_code}, " + f"typeName={type_name}, barCode={bar_code}" + ) + vial_bottles.append({ + "materialId": material_id, + "locationId": location_id, + "orderCode": order_code, + "typeName": type_name, + "barCode": bar_code, + }) + else: + logger.debug( + f"[提取分液瓶] 候选物料不是分液瓶: typeName={type_name}, 跳过" + ) + except Exception as e: + logger.warning( + f"[提取分液瓶] ⚠️ 查询物料详情失败: materialId={material_id}, 错误={e}" + ) + + if vial_bottles: + logger.info( + f"[提取分液瓶] 订单 {order_code} 共找到 {len(vial_bottles)} 个分液瓶: " + f"{[v['typeName'] for v in vial_bottles]}" + ) + else: + logger.warning(f"[提取分液瓶] ❌ 未找到分液瓶: orderCode={order_code}") + + return vial_bottles + + def _export_order_csv(self, final_result: Dict, csv_export_path: str) -> str: + """ + 将配液分液结果导出为 CSV 文件 + + CSV 表头: + orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, + 目标配液质量比, 真实配液质量比, 时间 + + Args: + final_result: _submit_and_wait_orders 返回的完整结果 + csv_export_path: CSV 文件保存目录路径 + + Returns: + 生成的 CSV 文件完整路径 + """ + # 确保目录存在 + os.makedirs(csv_export_path, exist_ok=True) + + # 生成文件名 + time_date = datetime.now().strftime("%Y%m%d_%H%M%S") + csv_file = os.path.join(csv_export_path, f"electrolyte_orders_{time_date}.csv") + + # 从 final_result 提取数据 + reports = final_result.get("reports", []) + mass_ratios = final_result.get("mass_ratios", []) + prep_bottles = final_result.get("prep_bottles", []) + vial_bottles_all = final_result.get("vial_bottles", []) + + # 建立 orderCode → mass_ratio 的索引 + ratio_map = {} + for ratio_item in mass_ratios: + oc = ratio_item.get("orderCode") + if oc: + ratio_map[oc] = ratio_item + + # 建立 orderCode → prep_bottle 的索引 + prep_map = {} + for pb in prep_bottles: + if pb: + oc = pb.get("orderCode") + if oc: + prep_map[oc] = pb + + # 建立 orderCode → vial_bottles 的索引 + vial_map: Dict[str, List[Dict]] = {} + for vb_list in vial_bottles_all: + if isinstance(vb_list, list): + for vb in vb_list: + oc = vb.get("orderCode") + if oc: + vial_map.setdefault(oc, []).append(vb) + elif isinstance(vb_list, dict): + oc = vb_list.get("orderCode") + if oc: + vial_map.setdefault(oc, []).append(vb_list) + + export_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + logger.info(f"[CSV导出] 开始导出, 订单数={len(reports)}, 路径={csv_file}") + + with open(csv_file, "w", newline="", encoding="utf-8-sig") as f: + writer = csv.writer(f) + # 写表头 + writer.writerow([ + "orderCode", "orderName", + "配液瓶类型", "配液瓶二维码", + "分液瓶类型", "分液瓶二维码", + "目标配液质量比", "真实配液质量比", + "时间", + ]) + + for report in reports: + order_code = report.get("orderCode", "N/A") + order_name = report.get("orderName", "N/A") + + # 配液瓶信息 + prep_info = prep_map.get(order_code, {}) + prep_type = prep_info.get("typeName", "") + prep_barcode = prep_info.get("barCode", "") + + # 分液瓶信息(可能多个) + vial_list = vial_map.get(order_code, []) + if len(vial_list) == 0: + vial_type_str = "" + vial_barcode_str = "" + elif len(vial_list) == 1: + vial_type_str = vial_list[0].get("typeName", "") + vial_barcode_str = vial_list[0].get("barCode", "") + else: + # 多个分液瓶用JSON数组表示 + vial_type_str = json.dumps( + [v.get("typeName", "") for v in vial_list], + ensure_ascii=False, + ) + vial_barcode_str = json.dumps( + [v.get("barCode", "") for v in vial_list], + ensure_ascii=False, + ) + + # 质量比信息 + ratio_info = ratio_map.get(order_code, {}) + target_ratio = ratio_info.get("target_mass_ratio", {}) + real_ratio = ratio_info.get("real_mass_ratio", {}) + target_ratio_str = json.dumps(target_ratio, ensure_ascii=False) if target_ratio else "" + real_ratio_str = json.dumps(real_ratio, ensure_ascii=False) if real_ratio else "" + + writer.writerow([ + order_code, order_name, + prep_type, prep_barcode, + vial_type_str, vial_barcode_str, + target_ratio_str, real_ratio_str, + export_time, + ]) + + logger.info( + f"[CSV导出] 写入: orderCode={order_code}, " + f"配液瓶={prep_type}({prep_barcode}), " + f"分液瓶数={len(vial_list)}" + ) + + f.flush() + + logger.info(f"[CSV导出] ✅ 导出完成: {csv_file}") + return csv_file + def _query_material_info(self, material_id: str) -> Dict: """ 调用 LIMS API 2.4 查询物料详情 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 9b511f4e..ed3eb714 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -1661,7 +1661,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase): 'Time', 'open_circuit_voltage', 'pole_weight', 'assembly_time', 'assembly_pressure', 'electrolyte_volume', 'coin_num', 'electrolyte_code', 'coin_cell_code', - 'formulation_order_code', 'formulation_ratio' # ← 新增配方列 + 'orderName', 'prep_bottle_barcode', 'vial_bottle_barcodes', + 'target_mass_ratio', 'real_mass_ratio' ]) #立刻写入磁盘 csvfile.flush() @@ -1670,8 +1671,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase): writer = csv.writer(csvfile) # ========== 提取配方信息 ========== - formulation_order_code = "" - formulation_ratio_str = "" + formulation_order_name = "" + prep_bottle_barcode = "" + vial_bottle_barcodes = "" + target_ratio_str = "" + real_ratio_str = "" # 从 self._formulations_list 获取配方信息 if hasattr(self, '_formulations_list') and self._formulations_list: @@ -1699,19 +1703,21 @@ class CoinCellAssemblyWorkstation(WorkstationBase): # 从配方列表中获取对应配方 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),如果不存在则使用目标质量比 + formulation_order_name = formulation.get("orderName", "") + prep_bottle_barcode = formulation.get("prep_bottle_barcode", "") + vial_bottle_barcodes = formulation.get("vial_bottle_barcodes", "") + real_ratio = formulation.get("real_mass_ratio", {}) target_ratio = formulation.get("target_mass_ratio", {}) - 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 "" + target_ratio_str = json.dumps(target_ratio, ensure_ascii=False) if target_ratio else "" + real_ratio_str = json.dumps(real_ratio, ensure_ascii=False) if real_ratio else "" logger.info( f"[CSV写入] 电池 {data_battery_number}: 使用配方[{current_bottle_index}] " - f"orderCode={formulation_order_code}, 比例={formulation_ratio_str}" + f"orderName={formulation_order_name}, 配液瓶={prep_bottle_barcode}, 分液瓶={vial_bottle_barcodes}" ) else: logger.warning( @@ -1725,7 +1731,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase): timestamp, data_open_circuit_voltage, data_pole_weight, data_assembly_time, data_assembly_pressure, data_electrolyte_volume, data_coin_type, data_electrolyte_code, data_coin_cell_code, - formulation_order_code, formulation_ratio_str # ← 新增配方数据 + formulation_order_name, prep_bottle_barcode, vial_bottle_barcodes, + target_ratio_str, real_ratio_str ]) #立刻写入磁盘 csvfile.flush() diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index 9b3f293b..57f2e4a7 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -152,6 +152,7 @@ bioyond_cell: goal: {} goal_default: xlsx_path: null + csv_export_path: '' handles: output: - data_key: total_orders @@ -179,6 +180,10 @@ bioyond_cell: properties: xlsx_path: type: string + csv_export_path: + default: '' + description: CSV导出目录路径,为空则不导出 + type: string required: - xlsx_path type: object @@ -194,6 +199,7 @@ bioyond_cell: goal: {} goal_default: batch_id: '' + order_names: [] bottle_type: 配液小瓶 conductivity_bottle_count: 0 conductivity_volume: 0.0 @@ -201,6 +207,7 @@ bioyond_cell: coin_cell_volume: 0.0 mix_time: [] pouch_cell_volume: 0.0 + csv_export_path: '' handles: output: - data_key: total_orders @@ -231,6 +238,12 @@ bioyond_cell: default: '' description: 批次ID,为空则自动生成时间戳 type: string + order_names: + default: [] + description: 配方ID/订单编号列表,与formulation一一对应,用于填写DoE撒点编号等自定义标识,便于后续追溯。未填则自动生成。 + items: + type: string + type: array bottle_type: default: 配液小瓶 description: 配液瓶类型 @@ -283,6 +296,10 @@ bioyond_cell: default: 0.0 description: 软包电池注液组装分液体积 type: number + csv_export_path: + default: '' + description: CSV导出目录路径,为空则不导出 + type: string required: - formulation type: object