diff --git a/new_cellconfig3c.json b/new_cellconfig3c.json index 36190858..446d2357 100644 --- a/new_cellconfig3c.json +++ b/new_cellconfig3c.json @@ -3,34 +3,64 @@ { "id": "bioyond_cell_workstation", "name": "配液分液工站", - "children": [ - ], "parent": null, + "children": [ + "YB_Bioyond_Deck" + ], "type": "device", "class": "bioyond_cell", "config": { - "protocol_type": [], - "station_resource": {} + "deck": { + "data": { + "_resource_child_name": "YB_Bioyond_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck" + } + }, + "protocol_type": [] }, "data": {} - }, - { + }, + { + "id": "YB_Bioyond_Deck", + "name": "YB_Bioyond_Deck", + "children": [], + "parent": "bioyond_cell_workstation", + "type": "deck", + "class": "BIOYOND_YB_Deck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "BIOYOND_YB_Deck", + "setup": true, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + }, + { "id": "BatteryStation", "name": "扣电工作站", "children": [ - "coin_cell_deck" + "coin_cell_deck" ], "parent": null, "type": "device", "class": "coincellassemblyworkstation_device", "position": { - "x": 600, - "y": 400, - "z": 0 + "x": 600, + "y": 400, + "z": 0 }, "config": { - "debug_mode": false, - "protocol_type": [] + "debug_mode": false, + "protocol_type": [] } } ], 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 deab824f..ee825791 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,7 +11,6 @@ import re import threading import json from urllib3 import response -from unilabos.devices.workstation.workstation_base import WorkstationBase from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation, BioyondResourceSynchronizer from unilabos.devices.workstation.bioyond_studio.config import ( API_CONFIG, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, SOLID_LIQUID_MAPPINGS @@ -19,7 +18,6 @@ from unilabos.devices.workstation.bioyond_studio.config import ( from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService from unilabos.utils.log import logger from unilabos.registry.registry import lab_registry -from unilabos.resources.bioyond.decks import YB_Deck, BIOYOND_YB_Deck def _iso_local_now_ms() -> str: # 文档要求:到毫秒 + Z,例如 2025-08-15T05:43:22.814Z @@ -36,29 +34,25 @@ class BioyondCellWorkstation(BioyondWorkstation): 查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28) """ - def __init__( - self, - bioyond_config: Optional[Dict[str, Any]] = None, - deck: Optional[Dict[str, Any]] = None, - *args, **kwargs, - ): + def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs): # 使用统一配置,支持自定义覆盖, 从 config.py 加载完整配置 - self.bioyond_config = bioyond_config or { + self.bioyond_config ={ **API_CONFIG, "material_type_mappings": MATERIAL_TYPE_MAPPINGS, - "warehouse_mapping": WAREHOUSE_MAPPING + "warehouse_mapping": WAREHOUSE_MAPPING, + "debug_mode": False } # "material_type_mappings": MATERIAL_TYPE_MAPPINGS # "warehouse_mapping": WAREHOUSE_MAPPING - self.deck = BIOYOND_YB_Deck() - self.deck.setup() - print(self.bioyond_config) + if deck is None and config: + deck = config.get('deck') + # print(self.bioyond_config) self.debug_mode = self.bioyond_config["debug_mode"] self.http_service_started = self.debug_mode - self.device_id = kwargs.pop("device_id", "bioyond_cell_workstation") - super().__init__(bioyond_config=self.bioyond_config, deck=self.deck) + self._device_id = "bioyond_cell_workstation" # 默认值,后续会从_ros_node获取 + super().__init__(bioyond_config=config, deck=deck) self.update_push_ip() #直接修改奔耀端的报送ip地址 logger.info("已更新奔耀端推送 IP 地址") @@ -72,6 +66,13 @@ class BioyondCellWorkstation(BioyondWorkstation): self.last_order_code = None logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})") + @property + def device_id(self): + """获取设备ID,优先从_ros_node获取,否则返回默认值""" + if hasattr(self, '_ros_node') and self._ros_node is not None: + return getattr(self._ros_node, 'device_id', self._device_id) + return self._device_id + def _start_http_service(self): """启动 HTTP 服务""" host = self.bioyond_config.get("HTTP_host", "") diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_workstation copy.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_workstation copy.py deleted file mode 100644 index 864a9ddd..00000000 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_workstation copy.py +++ /dev/null @@ -1,715 +0,0 @@ -# -*- coding: utf-8 -*- -from typing import Dict, Any, List, Optional -from datetime import datetime, timezone -import requests -from pathlib import Path -import pandas as pd -import time -from datetime import datetime, timezone, timedelta -import re -import threading -from unilabos.devices.workstation.workstation_base import WorkstationBase -from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService -from unilabos.utils.log import logger -from pylabrobot.resources.deck import Deck - - -def _iso_utc_now_ms() -> str: - # 文档要求:到毫秒 + Z,例如 2025-08-15T05:43:22.814Z - dt = datetime.now(timezone.utc) - return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z" - - -class BioyondWorkstation(WorkstationBase): - """ - 集成 Bioyond LIMS 的工作站示例, - 覆盖:入库(2.17/2.18) → 新建实验(2.14) → 启动调度(2.7) → - 运行中推送:物料变更(2.24)、步骤完成(2.21)、订单完成(2.23) → - 查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28) - """ - - def __init__( - self, - bioyond_config: Optional[Dict[str, Any]] = None, - station_resource: Optional[Dict[str, Any]] = None, - debug_mode: bool = False, # 增加调试模式开关 - *args, **kwargs, - ): - self.bioyond_config = bioyond_config or { - "base_url": "http://192.168.1.200:44386", - "api_key": "8A819E5C", - "timeout": 30, - "report_token": "CHANGE_ME_TOKEN" - } - - self.http_service_started = False - self.debug_mode = debug_mode - super().__init__(deck=Deck, station_resource=station_resource, *args, **kwargs) - logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})") - - # 实例化并在后台线程启动 HTTP 报送服务 - self.order_status = {} - try: - t = threading.Thread(target=self._start_http_service_bg, daemon=True, name="unilab_http") - t.start() - - except Exception as e: - logger.error(f"unilab-server后台启动报送服务失败: {e}") - - @property - def device_id(self) -> str: - try: - return getattr(self, "_ros_node").device_id # 兼容 ROS 场景 - except Exception: - return "bioyond_workstation" - - def _start_http_service_bg(self, host: str = "192.168.1.104", port: int = 8080) -> None: - logger.info("进入 _start_http_service_bg 函数") - try: - self.service = WorkstationHTTPService(self, host=host, port=port) - logger.info("WorkstationHTTPService 实例化完成") - self.service.start() - self.http_service_started = True - logger.info(f"unilab_HTTP 服务成功启动: {host}:{port}") - - #一直挂着,直到进程退出 - while True: - time.sleep(1) - - except Exception as e: - self.http_service_started = False - logger.error(f"启动unilab_HTTP服务失败: {e}", exc_info=True) - - # -------------------- 基础HTTP封装 -------------------- - def _url(self, path: str) -> str: - return f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}" - - def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]: - """LIMS API:大多数接口用 {apiKey/requestTime,data} 包装""" - payload = { - "apiKey": self.bioyond_config["api_key"], - "requestTime": _iso_utc_now_ms() - } - if data is not None: - payload["data"] = data - - if self.debug_mode: - # 模拟返回,不发真实请求 - logger.info(f"[DEBUG] POST {path} with payload={payload}") - return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"} - - try: - r = requests.post( - self._url(path), - json=payload, - timeout=self.bioyond_config.get("timeout", 30), - headers={"Content-Type": "application/json"} - ) - r.raise_for_status() - return r.json() - except Exception as e: - logger.error(f"POST {path} 失败: {e}") - return {"error": str(e)} - - # --- 修正:_post_report / _post_report_raw 同样走 debug_mode --- - def _post_report(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]: - payload = { - "token": self.bioyond_config.get("report_token", ""), - "request_time": _iso_utc_now_ms(), - "data": data - } - if self.debug_mode: - logger.info(f"[DEBUG] POST {path} with payload={payload}") - return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"} - try: - r = requests.post(self._url(path), json=payload, - timeout=self.bioyond_config.get("timeout", 30), - headers={"Content-Type": "application/json"}) - r.raise_for_status() - return r.json() - except Exception as e: - logger.error(f"POST {path} 失败: {e}") - return {"error": str(e)} - - def _post_report_raw(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]: - if self.debug_mode: - logger.info(f"[DEBUG] POST {path} with body={body}") - return {"debug": True, "url": self._url(path), "payload": body, "status": "ok"} - try: - r = requests.post(self._url(path), json=body, - timeout=self.bioyond_config.get("timeout", 30), - headers={"Content-Type": "application/json"}) - r.raise_for_status() - return r.json() - except Exception as e: - logger.error(f"POST {path} 失败: {e}") - return {"error": str(e)} - - - # -------------------- 单点接口封装 -------------------- - # 2.17 入库物料(单个) - def storage_inbound(self, material_id: str, location_id: str) -> Dict[str, Any]: - return self._post_lims("/api/lims/storage/inbound", { - "materialId": material_id, - "locationId": location_id - }) - - # 2.18 批量入库(多个) - def storage_batch_inbound(self, items: List[Dict[str, str]]) -> Dict[str, Any]: - """ - items = [{"materialId": "...", "locationId": "..."}, ...] - """ - return self._post_lims("/api/lims/storage/batch-inbound", items) - - # 3.30 自动化上料(Excel -> JSON -> POST /api/lims/order/auto-feeding4to3) - def auto_feeding4to3_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]: - """ - 根据固定模板解析 Excel: - - 四号手套箱加样头面 (2-13行, 3-7列) - - 四号手套箱原液瓶面 (15-23行, 3-9列) - - 三号手套箱人工堆栈 (26-40行, 3-7列) - """ - path = Path(xlsx_path) - if not path.exists(): - raise FileNotFoundError(f"未找到 Excel 文件:{path}") - - try: - df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl") - except Exception as e: - raise RuntimeError(f"读取 Excel 失败:{e}") - - items: List[Dict[str, Any]] = [] - - # 四号手套箱 - 加样头面(2-13行, 3-7列) - for _, row in df.iloc[1:13, 2:7].iterrows(): - item = { - "sourceWHName": "四号手套箱堆栈", - "posX": int(row[2]), - "posY": int(row[3]), - "posZ": int(row[4]), - "materialName": str(row[5]).strip() if pd.notna(row[5]) else "", - "quantity": float(row[6]) if pd.notna(row[6]) else 0.0, - } - if item["materialName"]: - items.append(item) - - # 四号手套箱 - 原液瓶面(15-23行, 3-9列) - for _, row in df.iloc[14:23, 2:9].iterrows(): - item = { - "sourceWHName": "四号手套箱堆栈", - "posX": int(row[2]), - "posY": int(row[3]), - "posZ": int(row[4]), - "materialName": str(row[5]).strip() if pd.notna(row[5]) else "", - "quantity": float(row[6]) if pd.notna(row[6]) else 0.0, - "materialType": str(row[7]).strip() if pd.notna(row[7]) else "", - "targetWH": str(row[8]).strip() if pd.notna(row[8]) else "", - } - if item["materialName"]: - items.append(item) - - # 三号手套箱人工堆栈(26-40行, 3-7列) - for _, row in df.iloc[25:40, 2:7].iterrows(): - item = { - "sourceWHName": "三号手套箱人工堆栈", - "posX": int(row[2]), - "posY": int(row[3]), - "posZ": int(row[4]), - "materialType": str(row[5]).strip() if pd.notna(row[5]) else "", - "materialId": str(row[6]).strip() if pd.notna(row[6]) else "", - "quantity": 1 # 默认数量1 - } - if item["materialId"] or item["materialType"]: - items.append(item) - - return self._post_lims("/api/lims/order/auto-feeding4to3", items) - - - - def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]: - """ - 3.31 自动化下料(Excel -> JSON -> POST /api/lims/storage/auto-batch-out-bound) - """ - path = Path(xlsx_path) - if not path.exists(): - 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}") - - def pick(names: List[str]) -> Optional[str]: - for n in names: - if n in df.columns: - return n - return None - - c_loc = pick(["locationId", "库位ID", "库位Id", "库位id"]) - c_wh = pick(["warehouseId", "仓库ID", "仓库Id", "仓库id"]) - c_qty = pick(["数量", "quantity"]) - c_x = pick(["x", "X", "posX", "坐标X"]) - c_y = pick(["y", "Y", "posY", "坐标Y"]) - c_z = pick(["z", "Z", "posZ", "坐标Z"]) - - required = [c_loc, c_wh, c_qty, c_x, c_y, c_z] - if any(c is None for c in required): - raise KeyError("Excel 缺少必要列:locationId/warehouseId/数量/x/y/z(支持多别名,至少要能匹配到)。") - - def as_int(v, d=0): - try: - if pd.isna(v): return d - return int(v) - except Exception: - try: - return int(float(v)) - except Exception: - return d - - def as_float(v, d=0.0): - try: - if pd.isna(v): return d - return float(v) - except Exception: - return d - - def as_str(v, d=""): - if v is None or (isinstance(v, float) and pd.isna(v)): return d - s = str(v).strip() - return s if s else d - - items: List[Dict[str, Any]] = [] - for _, row in df.iterrows(): - items.append({ - "locationId": as_str(row[c_loc]), - "warehouseId": as_str(row[c_wh]), - "quantity": as_float(row[c_qty]), - "x": as_int(row[c_x]), - "y": as_int(row[c_y]), - "z": as_int(row[c_z]), - }) - - return self._post_lims("/api/lims/storage/auto-batch-out-bound", items) - - # 2.14 新建实验 - def create_orders(self, xlsx_path: str) -> Dict[str, Any]: - """ - 从 Excel 解析并创建实验(2.14) - 约定: - - batchId = Excel 文件名(不含扩展名) - - 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列) - - totalMass 自动计算为所有物料质量之和 - - createTime 缺失或为空时自动填充为当前日期(YYYY/M/D) - """ - path = Path(xlsx_path) - if not path.exists(): - 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}") - - # 列名容错:返回可选列名,找不到则返回 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"]) - - # 物料列:所有以 (g) 结尾 - material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")] - 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_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 - - 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_int(row[col_load]) if col_load else 0, - "pouchCellInfo": _as_int(row[col_pouch]) if col_pouch else 0, - "conductivityInfo": _as_int(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) # 自动汇总 - } - orders.append(order_data) - - # print(orders) - - response = self._post_lims("/api/lims/order/orders", orders) - self.order_status[response["data"]["orderCode"]] = "running" - - while True: - time.sleep(5) - if self.order_status.get(response["data"]["orderCode"], None) == "finished": - logger.info(f"配液实验已完成 ,即将执行 3-2-1 转运") - break - logger.info(f"等待配液实验完成") - - self.transfer_3_to_2_to_1() - r321 = self.wait_for_transfer_task() - logger.info(f"3-2-1 转运完成,返回结果") - return r321 - - - # 2.7 启动调度 - def scheduler_start(self) -> Dict[str, Any]: - return self._post_lims("/api/lims/scheduler/start") - # 3.10 停止调度 - def scheduler_stop(self) -> Dict[str, Any]: - """ - 停止调度 (3.10) - 请求体只包含 apiKey 和 requestTime - """ - return self._post_lims("/api/lims/scheduler/stop") - # 2.9 继续调度 - def scheduler_continue(self) -> Dict[str, Any]: - """ - 继续调度 (2.9) - 请求体只包含 apiKey 和 requestTime - """ - return self._post_lims("/api/lims/scheduler/continue") - - - - # 2.24 物料变更推送 - def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]: - """ - material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等) - """ - return self._post_report_raw("/report/material_change", material_obj) - - # 2.21 步骤完成推送(BS → LIMS) - def report_step_finish(self, - order_code: str, - order_name: str, - step_name: str, - step_id: str, - sample_id: str, - start_time: str, - end_time: str, - execution_status: str = "completed") -> Dict[str, Any]: - data = { - "orderCode": order_code, - "orderName": order_name, - "stepName": step_name, - "stepId": step_id, - "sampleId": sample_id, - "startTime": start_time, - "endTime": end_time, - "executionStatus": execution_status - } - return self._post_report("/report/step_finish", data) - - # 2.23 订单完成推送(BS → LIMS) - def report_order_finish(self, - order_code: str, - order_name: str, - start_time: str, - end_time: str, - status: str = "30", # 30 完成 / -11 异常停止 / -12 人工停止 - workflow_status: str = "Finished", - completion_time: Optional[str] = None, - used_materials: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]: - data = { - "orderCode": order_code, - "orderName": order_name, - "startTime": start_time, - "endTime": end_time, - "status": status, - "workflowStatus": workflow_status, - "completionTime": completion_time or end_time, - "usedMaterials": used_materials or [] - } - return self._post_report("/report/order_finish", data) - - # 2.5 批量查询实验报告(用于轮询是否完成) - def order_list(self, - status: Optional[str] = None, - begin_time: Optional[str] = None, - end_time: Optional[str] = None, - filter_text: Optional[str] = None, - skip: int = 0, page: int = 10) -> Dict[str, Any]: - data: Dict[str, Any] = {"skipCount": skip, "pageCount": page} - if status is not None: # 80 成功 / 90 失败 / 100 执行中 - data["status"] = status - if begin_time: - data["timeType"] = "CreationTime" - data["beginTime"] = begin_time - if end_time: - data["endTime"] = end_time - if filter_text: - data["filter"] = filter_text - return self._post_lims("/api/lims/order/order-list", data) - - # 2.6 实验报告查询(根据任务ID拿详情) - def order_report(self, order_id: str) -> Dict[str, Any]: - return self._post_lims("/api/lims/order/order-report", order_id) - - # 2.32 3-2-1 物料转运 - def transfer_3_to_2_to_1(self, - # source_wh_id: Optional[str] = None, - source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b', - source_x: int = 1, source_y: int = 1, source_z: int = 1) -> Dict[str, Any]: - payload: Dict[str, Any] = { - "sourcePosX": source_x, "sourcePosY": source_y, "sourcePosZ": source_z - } - if source_wh_id: - payload["sourceWHID"] = source_wh_id - return self._post_lims("/api/lims/order/transfer-task3To2To1", payload) - - # 2.28 样品/废料取出 - def take_out(self, - order_id: str, - preintake_ids: Optional[List[str]] = None, - material_ids: Optional[List[str]] = None) -> Dict[str, Any]: - data = { - "orderId": order_id, - "preintakeIds": preintake_ids or [], - "materialIds": material_ids or [] - } - return self._post_lims("/api/lims/order/take-out", data) - - # --------(可选)占位方法:文档未定义的“1号站内部流程 / 1-2转运”-------- - def start_station1_internal_flow(self, **kwargs) -> None: - logger.info("启动1号站内部流程(占位,按现场系统填充具体指令)") - - - # 3.x 1→2 物料转运 - def transfer_1_to_2(self) -> Dict[str, Any]: - """ - 1→2 物料转运 - URL: /api/lims/order/transfer-task1To2 - 只需要 apiKey 和 requestTime - """ - return self._post_lims("/api/lims/order/transfer-task1To2") - - - # -------------------- 整体编排 -------------------- - def run_full_workflow(self, - inbound_items: List[Dict[str, str]], - orders: List[Dict[str, Any]], - poll_filter_code: Optional[str] = None, - poll_timeout_s: int = 600, - poll_interval_s: int = 5, - transfer_source: Optional[Dict[str, Any]] = None, - takeout_order_id: Optional[str] = None) -> None: - """ - 一键串联: - 1) 入库 3-4 个物料 → 2) 新建实验 → 3) 启动调度 - 运行中(如需):4) 物料变更推送 5) 步骤完成推送 6) 订单完成推送 - 完成后:查询实验(2.5/2.6)→ 7) 3-2-1 转运 → 8) 1号站内部流程 - → 9) 1-2 转运 → 10) 样品/废料取出 - """ - # 1. 入库(多于1个就用批量接口 2.18) - if len(inbound_items) == 1: - r = self.storage_inbound(inbound_items[0]["materialId"], inbound_items[0]["locationId"]) - logger.info(f"单个入库结果: {r}") - else: - r = self.storage_batch_inbound(inbound_items) - logger.info(f"批量入库结果: {r}") - - # 2. 新建实验(2.14) - r = self.create_orders(orders) - logger.info(f"新建实验结果: {r}") - - # 3. 启动调度(2.7) - r = self.scheduler_start() - logger.info(f"启动调度结果: {r}") - - # —— 运行中各类推送(2.24 / 2.21 / 2.23),通常由实际任务驱动,这里提供调用方式 —— # - # self.report_material_change({...}) - # self.report_step_finish(order_code="BSO...", order_name="配液分液", step_name="xxx", step_id="...", sample_id="...", - # start_time=_iso_utc_now_ms(), end_time=_iso_utc_now_ms(), execution_status="completed") - # self.report_order_finish(order_code="BSO...", order_name="配液分液", start_time="...", end_time=_iso_utc_now_ms()) - - # 完成后才能转运:用 2.5 批量查询配合 filter=任务编码 轮询到 status=80(成功) - if poll_filter_code: - import time - deadline = time.time() + poll_timeout_s - while time.time() < deadline: - res = self.order_list(status="80", filter_text=poll_filter_code, page=5) - if isinstance(res, dict) and res.get("data", {}).get("items"): - logger.info(f"实验 {poll_filter_code} 已完成:{res['data']['items'][0]}") - break - time.sleep(poll_interval_s) - else: - logger.warning(f"等待实验 {poll_filter_code} 完成超时(未到 status=80)") - - # 7. 启动 3-2-1 转运(2.32) - if transfer_source: - r = self.transfer_3_to_2_to_1( - source_wh_id=transfer_source.get("sourceWHID"), - source_x=transfer_source.get("sourcePosX", 1), - source_y=transfer_source.get("sourcePosY", 1), - source_z=transfer_source.get("sourcePosZ", 1), - ) - logger.info(f"3-2-1 转运结果: {r}") - - # 8. 1号站内部流程(占位) - self.start_station1_internal_flow() - - # 9. 1→2 转运(占位) - self.transfer_1_to_2() - - # 10. 样品/废料取出(2.28) - if takeout_order_id: - r = self.take_out(order_id=takeout_order_id) - logger.info(f"样品/废料取出结果: {r}") - - # 2.5 批量查询实验报告 - def order_list_v2(self, - timeType: str = "string", - beginTime: str = "", - endTime: str = "", - status: str = "", - filter: str = "物料转移任务", - skipCount: int = 0, - pageCount: int = 1, - sorting: str = "") -> Dict[str, Any]: - """ - 批量查询实验报告的详细信息 (2.5) - URL: /api/lims/order/order-list - 参数默认值和接口文档保持一致 - """ - data: Dict[str, Any] = { - "timeType": timeType, - "beginTime": beginTime, - "endTime": endTime, - "status": status, - "filter": filter, - "skipCount": skipCount, - "pageCount": pageCount, - "sorting": sorting - } - return self._post_lims("/api/lims/order/order-list", data) - - - def wait_for_transfer_task(self, timeout: int = 600, interval: int = 3) -> bool: - """ - 轮询查询物料转移任务是否成功完成 (status=80) - - timeout: 最大等待秒数 (默认600秒) - - interval: 轮询间隔秒数 (默认3秒) - 返回 True 表示找到并成功完成,False 表示超时未找到 - """ - now = datetime.now() - beginTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") - endTime = (now + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ") - print(beginTime, endTime) - - deadline = time.time() + timeout - - while time.time() < deadline: - result = self.order_list_v2( - timeType="string", - beginTime=beginTime, - endTime=endTime, - status="", - filter="物料转移任务", - skipCount=0, - pageCount=1, - sorting="" - ) - print(result) - - items = result.get("data", {}).get("items", []) - for item in items: - name = item.get("name", "") - status = item.get("status") - if name.startswith("物料转移任务") and status == 80: - logger.info(f"硬件转移动作完成: {name}") - return True - - time.sleep(interval) - - logger.warning("超时未找到成功的物料转移任务") - return False - - -# -------------------------------- -if __name__ == "__main__": - ws = BioyondWorkstation() - # ws.scheduler_stop() - ws.scheduler_start() - logger.info("调度启动完成") - - # ws.scheduler_continue() - # 3.30 上料:读取模板 Excel 自动解析并 POST - r1 = ws.auto_feeding4to3_from_xlsx(r"C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板 (8).xlsx") - ws.wait_for_transfer_task() - logger.info("4号箱向3号箱转运物料转移任务已完成") - - # ws.scheduler_start() - # print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items - - # 新建实验 - res = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092501.xlsx") - # ws.scheduler_start() - # print(res) - - #1号站启动 - ws.transfer_1_to_2() - ws.wait_for_transfer_task() - logger.info("1号站向2号站转移任务完成") - logger.info("全流程结束") - - # 3.31 下料:同理 - # r2 = ws.auto_batch_outbound_from_xlsx(r"C:/path/样品导入模板 (8).xlsx") - # print(r2["payload"]["data"]) diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py index 6acc8f60..d609440d 100644 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ b/unilabos/devices/workstation/bioyond_studio/config.py @@ -17,7 +17,7 @@ API_CONFIG = { "report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"), # HTTP 服务配置 - "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.33.30"), # HTTP服务监听地址,监听计算机飞连ip地址 + "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.115"), # HTTP服务监听地址,监听计算机飞连ip地址 "HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")), "debug_mode": False,# 调试模式 } @@ -155,8 +155,9 @@ MATERIAL_TYPE_MAPPINGS = { "100ml液体": ("YB_1Bottle100mlCarrier", "d37166b3-ecaa-481e-bd84-3032b795ba07"), "液": ("YB_1BottleCarrier", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"), "高粘液": ("YB_1GaoNianYeBottleCarrier", "abe8df30-563d-43d2-85e0-cabec59ddc16"), - "加样头(大)": ("YB_jia_yang_tou_da", "3a190ca0-b2f6-9aeb-8067-547e72c11469"), - "加样头(大)板": ("YB_jia_yang_tou_da_1X1_carrier", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"), + # "加样头(大)": ("YB_jia_yang_tou_da", "3a190ca0-b2f6-9aeb-8067-547e72c11469"), + "加样头(大)": ("YB_jia_yang_tou_da_1X1_carrier", "3a190ca0-b2f6-9aeb-8067-547e72c11469"), + # "加样头(大)板": ("YB_jia_yang_tou_da_1X1_carrier", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"), "5ml分液瓶板": ("YB_6x5ml_DispensingVialCarrier", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"), "5ml分液瓶": ("YB_fen_ye_5ml_Bottle", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"), "20ml分液瓶板": ("YB_6x20ml_DispensingVialCarrier", "3a192fa4-47db-3449-162a-eaf8aba57e27"), diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index e2f4da88..af81d532 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -21,7 +21,6 @@ from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNo from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode from pylabrobot.resources.resource import Resource as ResourcePLR -from unilabos.resources.bioyond.decks import YB_Deck from unilabos.devices.workstation.bioyond_studio.config import ( API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING ) @@ -64,7 +63,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): logger.error("Bioyond API客户端未初始化") return False - bioyond_data = self.bioyond_api_client.stock_material('{"typeMode": 1, "includeDetail": true}') + bioyond_data = self.bioyond_api_client.stock_material('{"typeMode": 2, "includeDetail": true}') if not bioyond_data: logger.warning("从Bioyond获取的物料数据为空") return False @@ -138,7 +137,7 @@ class BioyondWorkstation(WorkstationBase): # 初始化父类 super().__init__( # 桌子 - deck=YB_Deck("YB_Deck14"), + deck=deck, *args, **kwargs, ) diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index d6988b7f..a9d754fc 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -137,7 +137,7 @@ bioyond_cell: WH4_x5_y1_z1_5_quantity: 0.0 WH4_x5_y2_z1_10_materialName: '' WH4_x5_y2_z1_10_quantity: 0.0 - xlsx_path: unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx + xlsx_path: C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx handles: {} placeholder_keys: {} result: {} @@ -463,7 +463,7 @@ bioyond_cell: default: 0.0 type: number xlsx_path: - default: unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx + default: C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx type: string required: [] type: object @@ -473,31 +473,6 @@ bioyond_cell: title: auto_feeding4to3参数 type: object type: UniLabJsonCommand - auto-auto_feeding4to3_from_xlsx: - feedback: {} - goal: {} - goal_default: - xlsx_path: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - xlsx_path: - type: string - required: - - xlsx_path - type: object - result: {} - required: - - goal - title: auto_feeding4to3_from_xlsx参数 - type: object - type: UniLabJsonCommand auto-create_and_inbound_materials: feedback: {} goal: {} @@ -1039,7 +1014,7 @@ bioyond_cell: goal: {} goal_default: order_code: null - timeout: 1800 + timeout: 36000 handles: {} placeholder_keys: {} result: {} @@ -1052,7 +1027,7 @@ bioyond_cell: order_code: type: string timeout: - default: 1800 + default: 36000 type: integer required: - order_code @@ -1105,9 +1080,11 @@ bioyond_cell: init_param_schema: config: properties: - bioyond_config: + config: + type: object + deck: type: string - station_resource: + protocol_type: type: string required: [] type: object diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index ef937050..664bab3e 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -22,13 +22,13 @@ BIOYOND_PolymerReactionStation_Deck: init_param_schema: {} registry_type: resource version: 1.0.0 -YB_Deck16: +BIOYOND_YB_Deck: category: - deck class: - module: unilabos.resources.bioyond.decks:YB_Deck + module: unilabos.resources.bioyond.decks:BIOYOND_YB_Deck type: pylabrobot - description: BIOYOND PolymerReactionStation Deck + description: BIOYOND_YB_Deck handles: [] icon: 配液站.webp init_param_schema: {} diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index fa242c3d..7bf1c38c 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -1,4 +1,5 @@ from os import name +from pickle import TRUE from pylabrobot.resources import Deck, Coordinate, Rotation from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, bioyond_warehouse_1x3x3, bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, bioyond_warehouse_5x1x1 @@ -71,7 +72,7 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck): class BIOYOND_YB_Deck(Deck): def __init__( self, - name: str = "YB_Deck", + name: str = "YB_Bioyond_Deck", size_x: float = 4150, size_y: float = 1400.0, size_z: float = 2670.0, @@ -115,10 +116,10 @@ class BIOYOND_YB_Deck(Deck): 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 +# def YB_Deck(name: str) -> Deck: +# # by=BIOYOND_YB_Deck(name=name) +# # by.setup() +# return None