Merge branch 'workstation_dev_YB3' into workstation_dev_YB3

This commit is contained in:
Calvin Cao
2025-10-24 13:56:00 +08:00
committed by GitHub
7 changed files with 363 additions and 495 deletions

View File

@@ -9,14 +9,13 @@ import time
from datetime import datetime, timedelta
import re
import threading
import os
import socket
import json
from urllib3 import response
from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation, BioyondResourceSynchronizer
from unilabos.devices.workstation.bioyond_studio.config import (
BIOYOND_FULL_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING
API_CONFIG, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, SOLID_LIQUID_MAPPINGS
)
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
from unilabos.utils.log import logger
@@ -43,32 +42,52 @@ class BioyondCellWorkstation(BioyondWorkstation):
*args, **kwargs,
):
# 使用统一配置,支持自定义覆盖
self.bioyond_config = bioyond_config or {
**BIOYOND_FULL_CONFIG, # 从 config.py 加载完整配置
"workflow_mappings": WORKFLOW_MAPPINGS,
# 使用统一配置,支持自定义覆盖, 从 config.py 加载完整配置
self.bioyond_config = bioyond_config or {
**API_CONFIG,
"material_type_mappings": MATERIAL_TYPE_MAPPINGS,
"warehouse_mapping": WAREHOUSE_MAPPING
}
}
# "material_type_mappings": MATERIAL_TYPE_MAPPINGS
# "warehouse_mapping": WAREHOUSE_MAPPING
print(self.bioyond_config)
self.debug_mode = self.bioyond_config["debug_mode"]
self.http_service_started = False
self.http_service_started = self.debug_mode
deck = kwargs.pop("deck", None)
self.device_id = kwargs.pop("device_id", "bioyond_cell_workstation")
super().__init__(bioyond_config=self.bioyond_config, deck=deck, station_resource=station_resource, *args, **kwargs)
# 步骤通量任务通知铃
self._pending_events: dict[str, threading.Event] = {}
self.update_push_ip() #直接修改奔耀端的报送ip地址
logger.info("已更新奔耀端推送 IP 地址")
# 启动 HTTP 服务线程
t = threading.Thread(target=self._start_http_service, daemon=True, name="unilab_http")
t.start()
logger.info("HTTP 服务线程已启动")
# 等到任务报送成功
self.order_finish_event = threading.Event()
self.last_order_status = None
self.last_order_code = None
logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})")
# 实例化并在后台线程启动 HTTP 报送服务
self.order_status = {} # 记录任务完成情况用于接受bioyond post信息和反馈信息尤其用于硬件查询和物料信息变化
def _start_http_service(self):
"""启动 HTTP 服务"""
host = self.bioyond_config.get("HTTP_host", "")
port = self.bioyond_config.get("HTTP_port", None)
try:
logger.info("准备开始unilab_HTTP后台线程")
t = threading.Thread(target=self._start_http_service, daemon=True, name="unilab_http")
t.start()
self.service = WorkstationHTTPService(self, host=host, port=port)
self.service.start()
self.http_service_started = True
logger.info(f"WorkstationHTTPService 成功启动: {host}:{port}")
while True:
time.sleep(1) #一直挂着,直到进程退出
except Exception as e:
logger.error(f"unilab-HTTP后台线程启动失败: {e}")
self.http_service_started = False
logger.error(f"启动 WorkstationHTTPService 失败: {e}", exc_info=True)
# http报送服务
# http报送服务返回数据部分
def process_step_finish_report(self, report_request):
stepId = report_request.data.get("stepId")
logger.info(f"步骤完成: stepId: {stepId}, stepName:{report_request.data.get('stepName')}")
@@ -82,147 +101,65 @@ class BioyondCellWorkstation(BioyondWorkstation):
order_code = report_request.data.get("orderCode")
status = report_request.data.get("status")
logger.info(f"report_request: {report_request}")
logger.info(f"任务完成: {order_code}, status={status}")
# 记录订单状态码
if order_code:
self.order_status[order_code] = status
# 保存完整报文
self.last_order_report = report_request.data
# 如果是当前等待的订单,触发事件
if self.last_order_code == order_code:
self.order_finish_event.set()
self._set_pending_event(order_code)
return {"status": "received"}
def _set_pending_event(self, taskname: Optional[str]) -> None:
if not taskname:
return
event = self._pending_events.get(taskname)
if event is None:
event = threading.Event()
self._pending_events[taskname] = event
event.set()
def _wait_for_order_completion(self, order_code: Optional[str], timeout: int = 600) -> bool:
def wait_for_order_finish(self, order_code: str, timeout: int = 1800) -> Dict[str, Any]:
"""
等待指定 orderCode 的 /report/order_finish 报送。
Args:
order_code: 任务编号
timeout: 超时时间(秒)
Returns:
完整的报送数据 + 状态判断结果
"""
if not order_code:
logger.warning("无法等待任务完成:order_code 为空")
return False
event = self._pending_events.get(order_code)
if event is None:
event = threading.Event()
self._pending_events[order_code] = event
elif event.is_set():
logger.info(f"任务 {order_code} 在等待之前已完成")
self._pending_events.pop(order_code, None)
return True
logger.info(f"等待任务 {order_code} 完成 (timeout={timeout}s)")
finished = event.wait(timeout)
if not finished:
logger.warning(f"等待任务 {order_code} 完成超时({timeout}s")
self._pending_events.pop(order_code, None)
return finished
logger.error("wait_for_order_finish() 被调用,但 order_code 为空")
return {"status": "error", "message": "empty order_code"}
def _wait_for_response_orders(self, response: Dict[str, Any], context: str, timeout: int = 600) -> None:
order_codes = self._extract_order_codes(response)
if not order_codes:
logger.warning(f"{context} 响应中未找到 orderCode无法跟踪任务完成")
return
for code in order_codes:
finished = self._wait_for_order_completion(code, timeout=timeout)
if finished:
# 检查订单返回码是否为30正常完成
status = self.order_status.get(code)
if status == 30 or status == "30":
logger.info(f"订单 {code} 成功完成,状态码: {status}")
else:
logger.warning(f"订单 {code} 完成但状态码异常: {status} (期望: 30, -11=异常停止, -12=人工停止)")
# 清理状态记录
self.order_status.pop(code, None)
else:
logger.error(f"订单 {code} 等待超时,未收到完成通知")
self.last_order_code = order_code
self.last_order_report = None
self.order_finish_event.clear()
@staticmethod
def _extract_order_codes(response: Dict[str, Any]) -> List[str]:
order_codes: List[str] = []
if not isinstance(response, dict):
return order_codes
data = response.get("data")
keys = ["orderCode", "order_code", "orderId", "order_id"]
if isinstance(data, dict):
for key in keys:
if key in data and data[key]:
order_codes.append(str(data[key]))
if not order_codes and "orders" in data and isinstance(data["orders"], list):
for order in data["orders"]:
if isinstance(order, dict):
for key in keys:
if key in order and order[key]:
order_codes.append(str(order[key]))
elif isinstance(data, list):
for item in data:
if isinstance(item, dict):
for key in keys:
if key in item and item[key]:
order_codes.append(str(item[key]))
elif isinstance(data, str):
if data:
order_codes.append(data)
meta = response.get("orderCode")
if meta:
order_codes.append(str(meta))
# 去重
seen = set()
unique_codes: List[str] = []
for code in order_codes:
if code not in seen:
seen.add(code)
unique_codes.append(code)
return unique_codes
logger.info(f"等待任务完成报送: orderCode={order_code} (timeout={timeout}s)")
if not self.order_finish_event.wait(timeout=timeout):
logger.error(f"等待任务超时: orderCode={order_code}")
return {"status": "timeout", "orderCode": order_code}
def _start_http_service(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
host = host or self.bioyond_config.get("HTTP_host", "")
port = port or self.bioyond_config.get("HTTP_port", )
# 报送数据匹配验证
report = self.last_order_report or {}
report_code = report.get("orderCode")
status = str(report.get("status", ""))
logger.info("准备开始unilab_HTTP服务")
try:
self.service = WorkstationHTTPService(self, host=host, port=port)
logger.info("WorkstationHTTPService 实例化完成")
self.service.start()
self.http_service_started = True
logger.info(f"WorkstationHTTPService成功启动: {host}:{port}")
if report_code != order_code:
logger.warning(f"收到的报送 orderCode 不匹配: {report_code}{order_code}")
return {"status": "mismatch", "report": report}
# 启动成功后上报本机推送地址3.36
try:
# 优先使用配置中的 report_ip
report_ip = self.bioyond_config.get("report_ip", "").strip()
if status == "30":
logger.info(f"任务成功完成 (orderCode={order_code})")
return {"status": "success", "report": report}
elif status == "-11":
logger.error(f"任务异常停止 (orderCode={order_code})")
return {"status": "abnormal_stop", "report": report}
elif status == "-12":
logger.warning(f"任务人工停止 (orderCode={order_code})")
return {"status": "manual_stop", "report": report}
else:
logger.warning(f"任务未知状态 ({status}) (orderCode={order_code})")
return {"status": f"unknown_{status}", "report": report}
# 如果配置中没有指定 report_ip且监听地址是 0.0.0.0,则自动检测
if not report_ip and host in ("0.0.0.0", ""):
# 从 Bioyond 配置中提取服务器地址
bioyond_server = self.bioyond_config.get("base_url", "")
if bioyond_server:
import urllib.parse
parsed = urllib.parse.urlparse(bioyond_server)
elif not report_ip:
# 如果没有配置 report_ip使用监听地址
report_ip = host
r = self.update_push_ip(report_ip, port)
logger.info(f"向 Bioyond 报送推送地址: {report_ip}:{port}, 结果: {r}")
except Exception as e:
logger.warning(f"调用更新推送IP接口失败: {e}")
#一直挂着,直到进程退出
while True:
time.sleep(1)
except Exception as e:
self.http_service_started = False # 调试用
logger.error(f"启动WorkstationHTTPService失败: {e}", exc_info=True)
# -------------------- 基础HTTP封装 --------------------
def _url(self, path: str) -> str:
return f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}"
return f"{self.bioyond_config['api_host'].rstrip('/')}/{path.lstrip('/')}"
def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]:
"""LIMS API大多数接口用 {apiKey/requestTime,data} 包装"""
@@ -236,9 +173,11 @@ class BioyondCellWorkstation(BioyondWorkstation):
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:
logger.info(json.dumps(payload, ensure_ascii=False))
response = requests.post(
self._url(path),
json=payload,
@@ -248,7 +187,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
response.raise_for_status()
return response.json()
except Exception as e:
logger.info(f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}")
logger.info(f"{self.bioyond_config['api_host'].rstrip('/')}/{path.lstrip('/')}")
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
@@ -263,7 +202,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
if self.debug_mode:
logger.info(f"[DEBUG] PUT {path} with payload={payload}")
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
return {"debug_mode": True, "url": self._url(path), "payload": payload, "status": "ok"}
try:
response = requests.put(
@@ -275,7 +214,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
response.raise_for_status()
return response.json()
except Exception as e:
logger.info(f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}")
logger.info(f"{self.bioyond_config['api_host'].rstrip('/')}/{path.lstrip('/')}")
logger.error(f"PUT {path} 失败: {e}")
return {"error": str(e)}
@@ -310,11 +249,10 @@ class BioyondCellWorkstation(BioyondWorkstation):
return self._post_lims("/api/lims/storage/batch-inbound", items)
def auto_feeding4to3(
self,
# ★ 修改点:默认模板路径
xlsx_path: Optional[str] = "/Users/calvincao/Desktop/work/uni-lab-all/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/样品导入模板.xlsx",
xlsx_path: Optional[str] = "unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\样品导入模板.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,
@@ -443,9 +381,15 @@ class BioyondCellWorkstation(BioyondWorkstation):
return {"code": 0, "message": "no valid items", "data": []}
logger.info(items)
response = self._post_lims("/api/lims/order/auto-feeding4to3", items)
self._wait_for_response_orders(response, "auto_feeding4to3")
return response
# 等待任务报送成功
order_code = response.get("data", {}).get("orderCode")
if not order_code:
logger.error("上料任务未返回有效 orderCode")
return response
# 等待完成报送
result = self.wait_for_order_finish(order_code)
return result
@@ -681,31 +625,17 @@ class BioyondCellWorkstation(BioyondWorkstation):
}
orders.append(order_data)
# print(orders)
while True:
time.sleep(5)
response = self._post_lims("/api/lims/order/orders", orders)
if response.get("data", []):
break
logger.info(f"等待配液实验创建完成")
# 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()
# self.wait_for_transfer_task()
# logger.info(f"3-2-1 转运完成,返回结果")
# return r321
self._wait_for_response_orders(response, "create_orders", timeout=1800)
return response
response = self._post_lims("/api/lims/order/orders", orders)
print(response)
# 等待任务报送成功
order_code = response.get("data", {}).get("orderCode")
if not order_code:
logger.error("上料任务未返回有效 orderCode")
return response
# 等待完成报送
result = self.wait_for_order_finish(order_code)
return result
# 2.7 启动调度
def scheduler_start(self) -> Dict[str, Any]:
@@ -726,6 +656,13 @@ class BioyondCellWorkstation(BioyondWorkstation):
请求体只包含 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/scheduler/continue")
def scheduler_reset(self) -> Dict[str, Any]:
"""
复位调度 (2.11)
请求体只包含 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/scheduler/reset")
# 2.24 物料变更推送
def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]:
@@ -744,7 +681,16 @@ class BioyondCellWorkstation(BioyondWorkstation):
}
if source_wh_id:
payload["sourceWHID"] = source_wh_id
return self._post_lims("/api/lims/order/transfer-task3To2To1", payload)
response = self._post_lims("/api/lims/order/transfer-task3To2To1", payload)
# 等待任务报送成功
order_code = response.get("data", {}).get("orderCode")
if not order_code:
logger.error("上料任务未返回有效 orderCode")
return response
# 等待完成报送
result = self.wait_for_order_finish(order_code)
return result
# 3.35 1→2 物料转运
def transfer_1_to_2(self) -> Dict[str, Any]:
@@ -753,7 +699,15 @@ class BioyondCellWorkstation(BioyondWorkstation):
URL: /api/lims/order/transfer-task1To2
只需要 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/order/transfer-task1To2")
response = self._post_lims("/api/lims/order/transfer-task1To2")
# 等待任务报送成功
order_code = response.get("data", {}).get("orderCode")
if not order_code:
logger.error("上料任务未返回有效 orderCode")
return response
# 等待完成报送
result = self.wait_for_order_finish(order_code)
return result
# 2.5 批量查询实验报告(post过滤关键字查询)
def order_list_v2(self,
@@ -825,28 +779,23 @@ class BioyondCellWorkstation(BioyondWorkstation):
logger.warning("超时未找到成功的物料转移任务")
return False
def create_solid_materials(self, material_names: List[str], type_id: str = "3a190ca0-b2f6-9aeb-8067-547e72c11469") -> List[Dict[str, Any]]:
def create_materials(self, mappings: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
批量创建固体物料
Args:
material_names: 物料名称列表
type_id: 物料类型ID默认为固体物料类型
Returns:
创建的物料列表每个元素包含物料信息和ID
将 SOLID_LIQUID_MAPPINGS 中的所有物料逐个 POST 到 /api/lims/storage/material
"""
created_materials = []
total = len(material_names)
results = []
for i, name in enumerate(material_names, 1):
# 根据接口文档构建完整的请求体
material_data = {
"typeId": type_id,
"name": name,
"unit": "g", # 添加单位
"quantity": 1, # 添加数量默认1
"parameters": "" # 参数字段(空字符串表示无参数)
for name, data in mappings.items():
data = {
"typeId": data["typeId"],
"code": data.get("code", ""),
"barCode": data.get("barCode", ""),
"name": data["name"],
"unit": data.get("unit", "g"),
"parameters": data.get("parameters", ""),
"quantity": data.get("quantity", ""),
"warningQuantity": data.get("warningQuantity", ""),
"details": data.get("details", [])
}
logger.info(f"正在创建第 {i}/{total} 个固体物料: {name}")
@@ -886,180 +835,132 @@ class BioyondCellWorkstation(BioyondWorkstation):
logger.info(f"物料创建完成,成功创建 {len(created_materials)}/{total} 个固体物料")
return created_materials
def create_and_inbound_materials_from_csv(
def _sync_materials_safe(self) -> bool:
"""仅使用 BioyondResourceSynchronizer 执行同步(与 station.py 保持一致)。"""
if hasattr(self, 'resource_synchronizer') and self.resource_synchronizer:
try:
return bool(self.resource_synchronizer.sync_from_external())
except Exception as e:
logger.error(f"同步失败: {e}")
return False
logger.warning("资源同步器未初始化")
return False
def _load_warehouse_locations(self, warehouse_name: str) -> tuple[List[str], List[str]]:
"""从配置加载仓库位置信息
Args:
warehouse_name: 仓库名称
Returns:
(location_ids, position_names) 元组
"""
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", WAREHOUSE_MAPPING)
if warehouse_name not in warehouse_mapping:
raise ValueError(f"配置中未找到仓库: {warehouse_name}。可用: {list(warehouse_mapping.keys())}")
site_uuids = warehouse_mapping[warehouse_name].get("site_uuids", {})
if not site_uuids:
raise ValueError(f"仓库 {warehouse_name} 没有配置位置")
# 按顺序获取位置ID和名称
location_ids = []
position_names = []
for key in sorted(site_uuids.keys()):
location_ids.append(site_uuids[key])
position_names.append(key)
return location_ids, position_names
def create_and_inbound_materials(
self,
csv_path: str = "solid_materials.csv",
material_names: Optional[List[str]] = None,
type_id: str = "3a190ca0-b2f6-9aeb-8067-547e72c11469",
warehouse_name: str = "粉末加样头堆栈"
) -> Dict[str, Any]:
"""
从CSV文件读取物料列表创建物料并批量入库到指定堆栈
传参与默认列表方式创建物料并入库不使用CSV
Args:
csv_path: CSV文件路径
type_id: 物料类型ID(默认为固体物料类型)
warehouse_name: 仓库名称(默认为"粉末加样头堆栈"
material_names: 物料名称列表;默认使用 [LiPF6, LiDFOB, DTD, LiFSI, LiPO2F2]
type_id: 物料类型ID
warehouse_name: 目标仓库名(用于取位置信息
Returns:
包含执行结果字典
执行结果字典
"""
logger.info("=" * 60)
logger.info(f"开始执行:从CSV读取物料列表并批量创建入库到 {warehouse_name}")
logger.info(f"开始执行:从参数创建物料并批量入库到 {warehouse_name}")
logger.info("=" * 60)
# 从配置中获取位置ID列表
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", WAREHOUSE_MAPPING)
if warehouse_name not in warehouse_mapping:
error_msg = f"配置中未找到仓库: {warehouse_name}"
logger.error(error_msg)
logger.info(f"可用的仓库: {list(warehouse_mapping.keys())}")
return {"success": False, "error": error_msg}
warehouse_config = warehouse_mapping[warehouse_name]
site_uuids = warehouse_config.get("site_uuids", {})
if not site_uuids:
error_msg = f"仓库 {warehouse_name} 没有配置位置"
logger.error(error_msg)
return {"success": False, "error": error_msg}
# 按顺序获取位置IDA01, B01, C01...
all_location_ids = []
position_names = []
for key in sorted(site_uuids.keys()):
all_location_ids.append(site_uuids[key])
position_names.append(key)
logger.info(f"✓ 从配置文件加载 {len(all_location_ids)} 个位置")
logger.info(f" 仓库: {warehouse_name}")
logger.info(f" 位置范围: {position_names[0]} ~ {position_names[-1]}")
# 读取CSV文件
csv_file_path = Path(csv_path)
material_names = []
try:
df_materials = pd.read_csv(csv_file_path)
if 'material_name' in df_materials.columns:
material_names = df_materials['material_name'].dropna().astype(str).str.strip().tolist()
logger.info(f"✓ 成功从CSV文件读取 {len(material_names)} 个物料名称")
logger.info(f" 文件路径: {csv_file_path}")
# 1) 准备物料名称(默认值)
default_materials = ["LiPF6", "LiDFOB", "DTD", "LiFSI", "LiPO2F2"]
mat_names = [m.strip() for m in (material_names or default_materials) if str(m).strip()]
if not mat_names:
return {"success": False, "error": "物料名称列表为空"}
# 2) 加载仓库位置信息
all_location_ids, position_names = self._load_warehouse_locations(warehouse_name)
logger.info(f"✓ 加载 {len(all_location_ids)} 个位置 ({position_names[0]} ~ {position_names[-1]})")
# 限制数量不超过可用位置
if len(mat_names) > len(all_location_ids):
logger.warning(f"物料数量超出位置数量,仅处理前 {len(all_location_ids)}")
mat_names = mat_names[:len(all_location_ids)]
# 3) 创建物料
logger.info(f"\n【步骤1/3】创建 {len(mat_names)} 个固体物料...")
created_materials = self.create_solid_materials(mat_names, type_id)
if not created_materials:
return {"success": False, "error": "没有成功创建任何物料"}
# 4) 批量入库
logger.info(f"\n【步骤2/3】批量入库物料...")
location_ids = all_location_ids[:len(created_materials)]
selected_positions = position_names[:len(created_materials)]
inbound_items = [
{"materialId": mat["materialId"], "locationId": loc_id}
for mat, loc_id in zip(created_materials, location_ids)
]
for material, position in zip(created_materials, selected_positions):
logger.info(f" - {material['name']}{position}")
result = self.storage_batch_inbound(inbound_items)
if result.get("code") != 1:
logger.error(f"✗ 批量入库失败: {result}")
return {"success": False, "error": "批量入库失败", "created_materials": created_materials, "inbound_result": result}
logger.info("✓ 批量入库成功")
# 5) 同步
logger.info(f"\n【步骤3/3】同步物料数据...")
if self._sync_materials_safe():
logger.info("✓ 物料数据同步完成")
else:
logger.error(f"✗ CSV文件缺少 'material_name'")
return {"success": False, "error": "CSV文件缺少 'material_name'"}
except FileNotFoundError:
logger.error(f"✗ 未找到CSV文件: {csv_file_path}")
logger.info("请创建CSV文件格式")
logger.info(" material_name")
logger.info(" LiPF6")
logger.info(" LiDFOB")
logger.info(" ...")
return {"success": False, "error": f"未找到CSV文件: {csv_file_path}"}
logger.warning("⚠ 物料数据同步未完成(可忽略,不影响已创建与入库的数据)")
logger.info("\n" + "=" * 60)
logger.info("流程完成")
logger.info("=" * 60 + "\n")
return {
"success": True,
"created_materials": created_materials,
"inbound_result": result,
"total_created": len(created_materials),
"total_inbound": len(inbound_items),
"warehouse": warehouse_name,
"positions": selected_positions
}
except Exception as e:
logger.error(f"读取CSV文件失败: {e}")
return {"success": False, "error": f"读取CSV文件失败: {e}"}
if not material_names:
logger.error("CSV文件中没有有效的物料名称")
return {"success": False, "error": "CSV文件中没有有效的物料名称"}
# 检查物料数量
if len(material_names) > len(all_location_ids):
logger.warning(f"物料数量({len(material_names)})超过可用位置数量({len(all_location_ids)})")
logger.warning(f"将仅创建前 {len(all_location_ids)} 个物料")
material_names = material_names[:len(all_location_ids)]
# 准备位置信息
location_ids = all_location_ids[:len(material_names)]
selected_positions = position_names[:len(material_names)]
# 步骤1: 创建固体物料
logger.info(f"\n【步骤1/2】创建 {len(material_names)} 个固体物料...")
logger.info(f"物料类型ID: {type_id}")
logger.info(f"物料列表: {', '.join(material_names)}")
created_materials = self.create_solid_materials(
material_names=material_names,
type_id=type_id
)
if len(created_materials) != len(material_names):
logger.warning(f"创建的物料数量({len(created_materials)})与计划数量({len(material_names)})不匹配!")
logger.warning("将仅对成功创建的物料进行入库操作")
if not created_materials:
logger.error("没有成功创建任何物料")
return {"success": False, "error": "没有成功创建任何物料"}
# 步骤2: 批量入库到指定位置
logger.info(f"\n【步骤2/2】批量入库物料到 {warehouse_name}...")
inbound_items = []
for idx, material in enumerate(created_materials):
if idx < len(location_ids):
inbound_items.append({
"materialId": material["materialId"],
"locationId": location_ids[idx]
})
logger.info(f" - {material['name']} (ID: {material['materialId'][:8]}...) → 位置 {selected_positions[idx]}")
logger.info(f"\n正在执行批量入库,共 {len(inbound_items)} 条记录...")
result = self.storage_batch_inbound(inbound_items)
inbound_success = result.get("code") == 1
if inbound_success:
logger.info(f"✓ 批量入库成功!")
logger.info(f" 响应数据: {result.get('data', {})}")
# 步骤3: 同步物料数据
logger.info(f"\n【步骤3/3】同步物料数据到系统...")
if hasattr(self, 'resource_synchronizer') and self.resource_synchronizer:
try:
# 尝试同步不同类型的物料
# typeMode: 0=耗材, 1=样品, 2=试剂
sync_success = False
for type_mode in [0, 1, 2]:
try:
logger.info(f" 尝试同步 typeMode={type_mode} 的物料...")
bioyond_data = self.hardware_interface.stock_material(
f'{{"typeMode": {type_mode}, "includeDetail": true}}'
)
if bioyond_data:
logger.info(f" ✓ 获取到 {len(bioyond_data) if isinstance(bioyond_data, list) else 1} 条物料数据")
sync_success = True
except Exception as e:
logger.debug(f" typeMode={type_mode} 同步失败: {e}")
continue
if sync_success:
logger.info(f"✓ 物料数据同步完成")
else:
logger.warning(f"⚠ 物料数据同步未获取到数据(这是正常的,新创建的物料可能需要时间才能查询到)")
except Exception as e:
logger.warning(f"⚠ 物料数据同步出错: {e}")
logger.info(f" 提示:新创建的物料已成功入库,同步失败不影响使用")
else:
logger.warning("⚠ 资源同步器未初始化,跳过同步")
else:
logger.error(f"✗ 批量入库失败!")
logger.error(f" 响应: {result}")
logger.info("\n" + "=" * 60)
logger.info("固体物料创建和入库流程完成")
logger.info("=" * 60 + "\n")
return {
"success": inbound_success,
"created_materials": created_materials,
"inbound_result": result,
"total_created": len(created_materials),
"total_inbound": len(inbound_items),
"warehouse": warehouse_name,
"positions": selected_positions
}
logger.error(f"执行失败: {e}")
return {"success": False, "error": str(e)}
# --------------------------------
@@ -1067,20 +968,24 @@ class BioyondCellWorkstation(BioyondWorkstation):
if __name__ == "__main__":
ws = BioyondCellWorkstation()
logger.info(ws.scheduler_start())
logger.info(ws.scheduler_stop())
results = ws.create_materials(SOLID_LIQUID_MAPPINGS)
for r in results:
logger.info(r)
# 从CSV文件读取物料列表并批量创建入库
result = ws.create_and_inbound_materials_from_csv()
result = ws.create_and_inbound_materials()
# 继续后续流程
logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱
# 使用正斜杠或 Path 对象来指定文件路径
excel_path = Path("unilabos/devices/workstation/bioyond_studio/bioyond_cell/2025092701.xlsx")
logger.info(ws.create_orders(excel_path))
logger.info(ws.transfer_3_to_2_to_1())
logger.info(ws.transfer_1_to_2())
# logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱
# # 使用正斜杠或 Path 对象来指定文件路径
# excel_path = Path("unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\2025092701.xlsx")
# logger.info(ws.create_orders(excel_path))
# logger.info(ws.transfer_3_to_2_to_1())
# logger.info(ws.transfer_1_to_2())
# logger.info(ws.scheduler_start())
while True:

View File

@@ -5,17 +5,13 @@
import os
# ==================== API 基础配置 ====================
# 支持通过环境变量覆盖默认值
API_CONFIG = {
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.11.219:44388"),
}
# ==================== 完整的 Bioyond 配置 ====================
# BioyondCellWorkstation 默认配置(包含所有必需参数)
BIOYOND_FULL_CONFIG = {
API_CONFIG = {
# API 连接配置
"base_url": os.getenv("BIOYOND_API_HOST", "http://172.16.11.219:44388"),
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.11.219:44388"),
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")),
@@ -27,91 +23,11 @@ BIOYOND_FULL_CONFIG = {
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
"report_ip": os.getenv("BIOYOND_REPORT_IP", "172.21.32.172"), # 报送给 Bioyond 的本机IP地址留空则自动检测
# 调试模式
"debug_mode": os.getenv("BIOYOND_DEBUG_MODE", "False").lower() == "true",
}
# 工作流映射配置
WORKFLOW_MAPPINGS = {
"reactor_taken_out": "",
"reactor_taken_in": "",
"Solid_feeding_vials": "",
"Liquid_feeding_vials(non-titration)": "",
"Liquid_feeding_solvents": "",
"Liquid_feeding(titration)": "",
"liquid_feeding_beaker": "",
"Drip_back": "",
}
# 工作流名称到DisplaySectionName的映射
WORKFLOW_TO_SECTION_MAP = {
'reactor_taken_in': '反应器放入',
'liquid_feeding_beaker': '液体投料-烧杯',
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
'Liquid_feeding_solvents': '液体投料-溶剂',
'Solid_feeding_vials': '固体投料-小瓶',
'Liquid_feeding(titration)': '液体投料-滴定',
'reactor_taken_out': '反应器取出'
"debug_mode": False,
}
# 库位映射配置
WAREHOUSE_MAPPING = {
"粉末堆栈": {
"uuid": "",
"site_uuids": {
# 样品板
"A1": "3a14198e-6929-31f0-8a22-0f98f72260df",
"A2": "3a14198e-6929-4379-affa-9a2935c17f99",
"A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
"A4": "3a14198e-6929-5e99-2b79-80720f7cfb54",
"B1": "3a14198e-6929-f525-9a1b-1857552b28ee",
"B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
"B3": "3a14198e-6929-2d86-a468-602175a2b5aa",
"B4": "3a14198e-6929-1a98-ae57-e97660c489ad",
# 分装板
"C1": "3a14198e-6929-46fe-841e-03dd753f1e4a",
"C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
"C3": "3a14198e-6929-72ac-32ce-9b50245682b8",
"C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
"D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
"D2": "3a14198e-6929-dde1-fc78-34a84b71afdf",
"D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
"D4": "3a14198e-6929-7ac8-915a-fea51cb2e884"
}
},
"溶液堆栈": {
"uuid": "",
"site_uuids": {
"A1": "3a14198e-d724-e036-afdc-2ae39a7f3383",
"A2": "3a14198e-d724-afa4-fc82-0ac8a9016791",
"A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
"A4": "3a14198e-d724-df6d-5e32-5483b3cab583",
"B1": "3a14198e-d724-d818-6d4f-5725191a24b5",
"B2": "3a14198e-d724-be8a-5e0b-012675e195c6",
"B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
"B4": "3a14198e-d724-1e28-c885-574c3df468d0",
"C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
"C2": "3a14198e-d724-ab4e-48cb-817c3c146707",
"C3": "3a14198e-d724-7f18-1853-39d0c62e1d33",
"C4": "3a14198e-d724-28a2-a760-baa896f46b66",
"D1": "3a14198e-d724-d378-d266-2508a224a19f",
"D2": "3a14198e-d724-f56e-468b-0110a8feb36a",
"D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
"D4": "3a14198e-d724-0ddd-9654-f9352a421de9"
}
},
"试剂堆栈": {
"uuid": "",
"site_uuids": {
"A1": "3a14198c-c2cf-8b40-af28-b467808f1c36",
"A2": "3a14198c-c2d0-f3e7-871a-e470d144296f",
"A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094",
"A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
"B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8",
"B2": "3a14198c-c2d0-1559-105d-0ea30682cab4",
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
}
},
"粉末加样头堆栈": {
"uuid": "",
"site_uuids": {
@@ -166,35 +82,80 @@ MATERIAL_TYPE_MAPPINGS = {
"枪头": ("YB_Pipette_Tip", "b6196971-1050-46da-9927-333e8dea062d"),
}
# 步骤参数配置各工作流的步骤UUID
WORKFLOW_STEP_IDS = {
"reactor_taken_in": {
"config": ""
SOLID_LIQUID_MAPPINGS = {
# 固体
"LiDFOB": {
"typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
"code": "",
"barCode": "",
"name": "LiDFOB",
"unit": "g",
"parameters": "",
"quantity": "2",
"warningQuantity": "1",
"details": []
},
"liquid_feeding_beaker": {
"liquid": "",
"observe": ""
},
"liquid_feeding_vials_non_titration": {
"liquid": "",
"observe": ""
},
"liquid_feeding_solvents": {
"liquid": "",
"observe": ""
},
"solid_feeding_vials": {
"feeding": "",
"observe": ""
},
"liquid_feeding_titration": {
"liquid": "",
"observe": ""
},
"drip_back": {
"liquid": "",
"observe": ""
}
# "LiPF6": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiPF6",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# "LiFSI": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiFSI",
# "unit": "g",
# "parameters": {"Density": "1.533"},
# "quantity": 2,
# "warningQuantity": 1,
# "details": [{}]
# },
# "DTC": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "DTC",
# "unit": "g",
# "parameters": {"Density": "1.533"},
# "quantity": 2,
# "warningQuantity": 1,
# "details": [{}]
# },
# "LiPO2F2": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiPO2F2",
# "unit": "g",
# "parameters": {"Density": "1.533"},
# "quantity": 2,
# "warningQuantity": 1,
# "details": [{}]
# },
# 液体
# "SA": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "VC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "AND": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "HTCN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DENE": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "TMSP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "TMSB": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "SN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "FEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
}
WORKFLOW_MAPPINGS = {}
LOCATION_MAPPING = {}

View File

@@ -71,11 +71,11 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
if content_length > 0:
post_data = self.rfile.read(content_length)
request_data = json.loads(post_data.decode('utf-8'))
else:
request_data = {}
logger.info(f"收到工作站报送: {endpoint} 收到接受数据:{request_data}")
# logger.info(f"收到的json数据: {request_data}")
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
# 统一的报送端点路由基于LIMS协议规范
if endpoint == '/report/step_finish':
response = self._handle_step_finish_report(request_data)
@@ -668,7 +668,7 @@ __all__ = [
if __name__ == "__main__":
# 简单测试HTTP服务
class BioyondWorkstation:
class DummyWorkstation:
device_id = "WS-001"
def process_step_finish_report(self, report_request):

View File

@@ -22,7 +22,7 @@ BIOYOND_PolymerReactionStation_Deck:
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_Deck15:
YB_Deck16:
category:
- deck
class:

View File

@@ -12,7 +12,7 @@ def YB_Solid_Stock(
"""创建粉末瓶"""
return Bottle(
name=name,
diameter=diameter,
diameter=diameter,# 未知
height=height,
max_volume=max_volume,
barcode=barcode,

View File

@@ -48,7 +48,7 @@ def bioyond_warehouse_1x2x2(name: str) -> WareHouse:
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
category="YB_warehouse",
)
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""

View File

@@ -624,6 +624,8 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
Returns:
pylabrobot 格式的物料列表
"""
print("1:bioyond_materials:",bioyond_materials)
# print("2:type_mapping:",type_mapping)
plr_materials = []
for material in bioyond_materials: