diff --git a/unilabos/device_comms/modbus_plc/client.py b/unilabos/device_comms/modbus_plc/client.py index 4b46746..c239b9d 100644 --- a/unilabos/device_comms/modbus_plc/client.py +++ b/unilabos/device_comms/modbus_plc/client.py @@ -4,7 +4,8 @@ import traceback from typing import Any, Union, List, Dict, Callable, Optional, Tuple from pydantic import BaseModel -from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient +from pymodbus.client import ModbusSerialClient, ModbusTcpClient +from pymodbus.framer import FramerType from typing import TypedDict from unilabos.device_comms.modbus_plc.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder @@ -402,7 +403,7 @@ class TCPClient(BaseClient): class RTUClient(BaseClient): def __init__(self, port: str, baudrate: int, timeout: int): super().__init__() - self._set_client(ModbusSerialClient(method='rtu', port=port, baudrate=baudrate, timeout=timeout)) + self._set_client(ModbusSerialClient(framer=FramerType.RTU, port=port, baudrate=baudrate, timeout=timeout)) self._connect() if __name__ == '__main__': diff --git a/unilabos/device_comms/modbus_plc/modbus.py b/unilabos/device_comms/modbus_plc/modbus.py index 7e384d7..028477e 100644 --- a/unilabos/device_comms/modbus_plc/modbus.py +++ b/unilabos/device_comms/modbus_plc/modbus.py @@ -1,26 +1,12 @@ # coding=utf-8 from enum import Enum from abc import ABC, abstractmethod -from typing import Tuple, Union, Optional, TYPE_CHECKING -from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder -from pymodbus.constants import Endian -if TYPE_CHECKING: - from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient - -# Define DataType enum for pymodbus 2.5.3 compatibility -class DataType(Enum): - INT16 = "int16" - UINT16 = "uint16" - INT32 = "int32" - UINT32 = "uint32" - INT64 = "int64" - UINT64 = "uint64" - FLOAT32 = "float32" - FLOAT64 = "float64" - STRING = "string" - BOOL = "bool" +from pymodbus.client import ModbusBaseSyncClient +from pymodbus.client.mixin import ModbusClientMixin +from typing import Tuple, Union, Optional +DataType = ModbusClientMixin.DATATYPE class WorderOrder(Enum): BIG = "big" @@ -33,96 +19,8 @@ class DeviceType(Enum): INPUT_REGISTER = 'input_register' -def _convert_from_registers(registers, data_type: DataType, word_order: str = 'big'): - """Convert registers to a value using BinaryPayloadDecoder. - - Args: - registers: List of register values - data_type: DataType enum specifying the target data type - word_order: 'big' or 'little' endian - - Returns: - Converted value - """ - # Determine byte and word order based on word_order parameter - if word_order == 'little': - byte_order = Endian.Little - word_order_enum = Endian.Little - else: - byte_order = Endian.Big - word_order_enum = Endian.Big - - decoder = BinaryPayloadDecoder.fromRegisters(registers, byteorder=byte_order, wordorder=word_order_enum) - - if data_type == DataType.INT16: - return decoder.decode_16bit_int() - elif data_type == DataType.UINT16: - return decoder.decode_16bit_uint() - elif data_type == DataType.INT32: - return decoder.decode_32bit_int() - elif data_type == DataType.UINT32: - return decoder.decode_32bit_uint() - elif data_type == DataType.INT64: - return decoder.decode_64bit_int() - elif data_type == DataType.UINT64: - return decoder.decode_64bit_uint() - elif data_type == DataType.FLOAT32: - return decoder.decode_32bit_float() - elif data_type == DataType.FLOAT64: - return decoder.decode_64bit_float() - elif data_type == DataType.STRING: - return decoder.decode_string(len(registers) * 2) - else: - raise ValueError(f"Unsupported data type: {data_type}") - - -def _convert_to_registers(value, data_type: DataType, word_order: str = 'little'): - """Convert a value to registers using BinaryPayloadBuilder. - - Args: - value: Value to convert - data_type: DataType enum specifying the source data type - word_order: 'big' or 'little' endian - - Returns: - List of register values - """ - # Determine byte and word order based on word_order parameter - if word_order == 'little': - byte_order = Endian.Little - word_order_enum = Endian.Little - else: - byte_order = Endian.Big - word_order_enum = Endian.Big - - builder = BinaryPayloadBuilder(byteorder=byte_order, wordorder=word_order_enum) - - if data_type == DataType.INT16: - builder.add_16bit_int(value) - elif data_type == DataType.UINT16: - builder.add_16bit_uint(value) - elif data_type == DataType.INT32: - builder.add_32bit_int(value) - elif data_type == DataType.UINT32: - builder.add_32bit_uint(value) - elif data_type == DataType.INT64: - builder.add_64bit_int(value) - elif data_type == DataType.UINT64: - builder.add_64bit_uint(value) - elif data_type == DataType.FLOAT32: - builder.add_32bit_float(value) - elif data_type == DataType.FLOAT64: - builder.add_64bit_float(value) - elif data_type == DataType.STRING: - builder.add_string(value) - else: - raise ValueError(f"Unsupported data type: {data_type}") - - return builder.to_registers() - - class Base(ABC): - def __init__(self, client, name: str, address: int, typ: DeviceType, data_type): + def __init__(self, client: ModbusBaseSyncClient, name: str, address: int, typ: DeviceType, data_type: DataType): self._address: int = address self._client = client self._name = name @@ -160,11 +58,7 @@ class Coil(Base): count = value, slave = slave) - # 检查是否读取出错 - if resp.isError(): - return [], True - - return resp.bits, False + return resp.bits, resp.isError() def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool: if isinstance(value, list): @@ -197,18 +91,8 @@ class DiscreteInputs(Base): count = value, slave = slave) - # 检查是否读取出错 - if resp.isError(): - # 根据数据类型返回默认值 - if data_type in [DataType.FLOAT32, DataType.FLOAT64]: - return 0.0, True - elif data_type == DataType.STRING: - return "", True - else: - return 0, True - # noinspection PyTypeChecker - return _convert_from_registers(resp.registers, data_type, word_order=word_order.value), False + return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError() def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool: raise ValueError('discrete inputs only support read') @@ -228,19 +112,8 @@ class HoldRegister(Base): address = self.address, count = value, slave = slave) - - # 检查是否读取出错 - if resp.isError(): - # 根据数据类型返回默认值 - if data_type in [DataType.FLOAT32, DataType.FLOAT64]: - return 0.0, True - elif data_type == DataType.STRING: - return "", True - else: - return 0, True - # noinspection PyTypeChecker - return _convert_from_registers(resp.registers, data_type, word_order=word_order.value), False + return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError() def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool: @@ -259,7 +132,7 @@ class HoldRegister(Base): return self._client.write_register(self.address, value, slave= slave).isError() else: # noinspection PyTypeChecker - encoder_resp = _convert_to_registers(value, data_type=data_type, word_order=word_order.value) + encoder_resp = self._client.convert_to_registers(value, data_type=data_type, word_order=word_order.value) return self._client.write_registers(self.address, encoder_resp, slave=slave).isError() @@ -280,19 +153,8 @@ class InputRegister(Base): address = self.address, count = value, slave = slave) - - # 检查是否读取出错 - if resp.isError(): - # 根据数据类型返回默认值 - if data_type in [DataType.FLOAT32, DataType.FLOAT64]: - return 0.0, True - elif data_type == DataType.STRING: - return "", True - else: - return 0, True - # noinspection PyTypeChecker - return _convert_from_registers(resp.registers, data_type, word_order=word_order.value), False + return self._client.convert_from_registers(resp.registers, data_type, word_order=word_order.value), resp.isError() def write(self,value: Union[int, float, bool, str, list[bool], list[int], list[float]], data_type: Optional[DataType ]= None, word_order: WorderOrder = WorderOrder.LITTLE, slave = 1) -> bool: raise ValueError('input register only support read') diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601081.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601081.xlsx new file mode 100644 index 0000000..16d7654 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601081.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601083.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601083.xlsx new file mode 100644 index 0000000..a6dd057 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601083.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch1.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch1.xlsx new file mode 100644 index 0000000..3bd3439 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch1.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch2.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch2.xlsx new file mode 100644 index 0000000..9ed74ec Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260108_multi_batch2.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601091.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601091.xlsx new file mode 100644 index 0000000..9d5162d Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/202601091.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation-2.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation-2.py deleted file mode 100644 index 6ba85a6..0000000 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation-2.py +++ /dev/null @@ -1,1234 +0,0 @@ -# -*- coding: utf-8 -*- -from cgi import print_arguments -from doctest import debug -from typing import Dict, Any, List, Optional -import requests -from pylabrobot.resources.resource import Resource as ResourcePLR -from pathlib import Path -import pandas as pd -import time -from datetime import datetime, timedelta -import re -import threading -import json -from copy import deepcopy -from urllib3 import response -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 -) -from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService -from unilabos.resources.bioyond.decks import BIOYOND_YB_Deck -from unilabos.utils.log import logger -from unilabos.registry.registry import lab_registry - -def _iso_local_now_ms() -> str: - # 文档要求:到毫秒 + Z,例如 2025-08-15T05:43:22.814Z - dt = datetime.now() - # print(dt) - return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z" - - -class BioyondCellWorkstation(BioyondWorkstation): - """ - 集成 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, config: dict = None, deck=None, protocol_type=None, **kwargs): - - # 使用统一配置,支持自定义覆盖, 从 config.py 加载完整配置 - self.bioyond_config ={ - **API_CONFIG, - "material_type_mappings": MATERIAL_TYPE_MAPPINGS, - "warehouse_mapping": WAREHOUSE_MAPPING, - "debug_mode": False - } - - # "material_type_mappings": MATERIAL_TYPE_MAPPINGS - # "warehouse_mapping": WAREHOUSE_MAPPING - 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 = "bioyond_cell_workstation" # 默认值,后续会从_ros_node获取 - super().__init__(bioyond_config=config, deck=deck) - 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})") - - @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", "") - port = self.bioyond_config.get("HTTP_port", None) - try: - 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: - self.http_service_started = False - logger.error(f"启动 WorkstationHTTPService 失败: {e}", exc_info=True) - - - # 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')}") - return report_request.data.get('executionStatus') - - def process_sample_finish_report(self, report_request): - logger.info(f"通量完成: {report_request.data.get('sampleId')}") - return {"status": "received"} - - def process_order_finish_report(self, report_request, used_materials=None): - 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}") - - # 保存完整报文 - self.last_order_report = report_request.data - # 如果是当前等待的订单,触发事件 - if self.last_order_code == order_code: - self.order_finish_event.set() - - return {"status": "received"} - - def wait_for_order_finish(self, order_code: str, timeout: int = 36000) -> Dict[str, Any]: - """ - 等待指定 orderCode 的 /report/order_finish 报送。 - Args: - order_code: 任务编号 - timeout: 超时时间(秒) - Returns: - 完整的报送数据 + 状态判断结果 - """ - if not order_code: - logger.error("wait_for_order_finish() 被调用,但 order_code 为空!") - return {"status": "error", "message": "empty order_code"} - - self.last_order_code = order_code - self.last_order_report = None - self.order_finish_event.clear() - - 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} - - # 报送数据匹配验证 - report = self.last_order_report or {} - report_code = report.get("orderCode") - status = str(report.get("status", "")) - - if report_code != order_code: - logger.warning(f"收到的报送 orderCode 不匹配: {report_code} ≠ {order_code}") - return {"status": "mismatch", "report": report} - - 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} - - - # -------------------- 基础HTTP封装 -------------------- - def _url(self, path: str) -> str: - 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} 包装""" - payload = { - "apiKey": self.bioyond_config["api_key"], - "requestTime": _iso_local_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: - logger.info(json.dumps(payload, ensure_ascii=False)) - response = requests.post( - self._url(path), - json=payload, - timeout=self.bioyond_config.get("timeout", 30), - headers={"Content-Type": "application/json"} - ) # 拼接网址+post bioyond接口 - response.raise_for_status() - return response.json() - except Exception as e: - logger.info(f"{self.bioyond_config['api_host'].rstrip('/')}/{path.lstrip('/')}") - logger.error(f"POST {path} 失败: {e}") - return {"error": str(e)} - - def _put_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]: - """LIMS API:PUT {apiKey/requestTime,data} 包装""" - payload = { - "apiKey": self.bioyond_config["api_key"], - "requestTime": _iso_local_now_ms() - } - if data is not None: - payload["data"] = data - - if self.debug_mode: - logger.info(f"[DEBUG] PUT {path} with payload={payload}") - return {"debug_mode": True, "url": self._url(path), "payload": payload, "status": "ok"} - - try: - response = requests.put( - self._url(path), - json=payload, - timeout=self.bioyond_config.get("timeout", 30), - headers={"Content-Type": "application/json"} - ) - response.raise_for_status() - return response.json() - except Exception as e: - logger.info(f"{self.bioyond_config['api_host'].rstrip('/')}/{path.lstrip('/')}") - logger.error(f"PUT {path} 失败: {e}") - return {"error": str(e)} - - # -------------------- 3.36 更新推送 IP 地址 -------------------- - def update_push_ip(self, ip: Optional[str] = None, port: Optional[int] = None) -> Dict[str, Any]: - """ - 3.36 更新推送 IP 地址接口(PUT) - URL: /api/lims/order/ip-config - 请求体:{ apiKey, requestTime, data: { ip, port } } - """ - target_ip = ip or self.bioyond_config.get("HTTP_host", "") - target_port = int(port or self.bioyond_config.get("HTTP_port", 0)) - data = {"ip": target_ip, "port": target_port} - - # 固定接口路径,不做其他路径兼容 - path = "/api/lims/order/ip-config" - return self._put_lims(path, data) - - # -------------------- 单点接口封装 -------------------- - # 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) - - - def auto_feeding4to3( - self, - # ★ 修改点:默认模板路径 - xlsx_path: Optional[str] = "/Users/sml/work/Unilab/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, - ): - """ - 自动化上料(支持两种模式) - - Excel 路径存在 → 从 Excel 模板解析 - - Excel 路径不存在 → 使用手动参数 - """ - items: List[Dict[str, Any]] = [] - - # ---------- 模式 1: Excel 导入 ---------- - if xlsx_path: - path = Path(__file__).parent / Path(xlsx_path) - if path.exists(): # ★ 修改点:路径存在才加载 - try: - df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl") - except Exception as e: - raise RuntimeError(f"读取 Excel 失败:{e}") - - # 四号手套箱加样头面 - for _, row in df.iloc[1:13, 2:7].iterrows(): - if pd.notna(row[5]): - items.append({ - "sourceWHName": "四号手套箱堆栈", - "posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]), - "materialName": str(row[5]).strip(), - "quantity": float(row[6]) if pd.notna(row[6]) else 0.0, - }) - # 四号手套箱原液瓶面 - for _, row in df.iloc[14:23, 2:9].iterrows(): - if pd.notna(row[5]): - items.append({ - "sourceWHName": "四号手套箱堆栈", - "posX": int(row[2]), "posY": int(row[3]), "posZ": int(row[4]), - "materialName": str(row[5]).strip(), - "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 "", - }) - # 三号手套箱人工堆栈 - for _, row in df.iloc[25:40, 2:7].iterrows(): - if pd.notna(row[5]) or pd.notna(row[6]): - items.append({ - "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 - }) - else: - logger.warning(f"未找到 Excel 文件 {xlsx_path},自动切换到手动参数模式。") - - # ---------- 模式 2: 手动填写 ---------- - if not items: - params = locals() - for name, value in params.items(): - if name.startswith("四号手套箱堆栈") and "materialName" in name and value: - idx = name.split("_") - items.append({ - "sourceWHName": "四号手套箱堆栈", - "posX": int(idx[1][1:]), "posY": int(idx[2][1:]), "posZ": int(idx[3][1:]), - "materialName": value, - "quantity": float(params.get(name.replace("materialName", "quantity"), 0.0)) - }) - elif name.startswith("四号手套箱堆栈") and "materialType" in name and (value or params.get(name.replace("materialType", "materialName"), "")): - idx = name.split("_") - items.append({ - "sourceWHName": "四号手套箱堆栈", - "posX": int(idx[1][1:]), "posY": int(idx[2][1:]), "posZ": int(idx[3][1:]), - "materialName": params.get(name.replace("materialType", "materialName"), ""), - "quantity": float(params.get(name.replace("materialType", "quantity"), 0.0)), - "materialType": value, - "targetWH": params.get(name.replace("materialType", "targetWH"), ""), - }) - elif name.startswith("三号手套箱人工堆栈") and "materialType" in name and (value or params.get(name.replace("materialType", "materialId"), "")): - idx = name.split("_") - items.append({ - "sourceWHName": "三号手套箱人工堆栈", - "posX": int(idx[1][1:]), "posY": int(idx[2][1:]), "posZ": int(idx[3][1:]), - "materialType": value, - "materialId": params.get(name.replace("materialType", "materialId"), ""), - "quantity": int(params.get(name.replace("materialType", "quantity"), 1)), - }) - - if not items: - logger.warning("没有有效的上料条目,已跳过提交。") - return {"code": 0, "message": "no valid items", "data": []} - logger.info(items) - response = self._post_lims("/api/lims/order/auto-feeding4to3", items) - - # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") - if not order_code: - logger.error("上料任务未返回有效 orderCode!") - return response - # 等待完成报送 - result = self.wait_for_order_finish(order_code) - print("\n" + "="*60) - print("实验记录本结果auto_feeding4to3") - print("="*60) - print(json.dumps(result, indent=2, ensure_ascii=False)) - print("="*60 + "\n") - return result - - 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]), - }) - - response = self._post_lims("/api/lims/storage/auto-batch-out-bound", items) - 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]: - """ - 从 Excel 解析并创建实验(2.14) - 约定: - - batchId = Excel 文件名(不含扩展名) - - 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列) - - totalMass 自动计算为所有物料质量之和 - - createTime 缺失或为空时自动填充为当前日期(YYYY/M/D) - """ - default_path = Path("/Users/sml/work/Unilab/unilabos/devices/workstation/bioyond_studio/bioyond_cell/2025092701.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)}") - response = self._post_lims("/api/lims/order/orders", orders) - print(f"[create_orders] 接口返回: {response}") - # 等待任务报送成功 - data_list = response.get("data", []) - if data_list: - order_code = data_list[0].get("orderCode") - else: - order_code = None - - if not order_code: - logger.error("上料任务未返回有效 orderCode!") - return response - # 等待完成报送 - result = self.wait_for_order_finish(order_code) - print("实验记录本========================create_orders========================") - print(result) - print("========================") - return result - - # 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 继续调度 - # 2.9 继续调度 - def scheduler_continue(self) -> Dict[str, Any]: - """ - 继续调度 (2.9) - 请求体只包含 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]: - """ - material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等) - """ - return self._post_report_raw("/report/material_change", material_obj) - - # 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 - - 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]: - """ - 1→2 物料转运 - URL: /api/lims/order/transfer-task1To2 - 只需要 apiKey 和 requestTime - """ - 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, - timeType: str = "", - beginTime: str = "", - endTime: str = "", - status: str = "", # 60表示正在运行,80表示完成,90表示失败 - 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) - - # 一直post执行bioyond接口查询任务状态 - def wait_for_transfer_task(self, timeout: int = 3000, interval: int = 5, filter_text: Optional[str] = None) -> 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="", - beginTime=beginTime, - endTime=endTime, - status="", - filter=filter_text, - 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") - # 改成用 filter_text 判断 - if (not filter_text or filter_text in name) and status == 80: - logger.info(f"硬件转移动作完成: {name}, status={status}") - return True - - logger.info(f"等待中: {name}, status={status}") - time.sleep(interval) - - logger.warning("超时未找到成功的物料转移任务") - return False - - def create_materials(self, mappings: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - 将 SOLID_LIQUID_MAPPINGS 中的所有物料逐个 POST 到 /api/lims/storage/material - """ - results = [] - - 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}") - result = self._post_lims("/api/lims/storage/material", material_data) - - if result and result.get("code") == 1: - # data 字段可能是字符串(物料ID)或字典(包含id字段) - data = result.get("data") - if isinstance(data, str): - # data 直接是物料ID字符串 - material_id = data - elif isinstance(data, dict): - # data 是字典,包含id字段 - material_id = data.get("id") - else: - material_id = None - - if material_id: - created_materials.append({ - "name": name, - "materialId": material_id, - "typeId": type_id - }) - logger.info(f"✓ 成功创建物料: {name}, ID: {material_id}") - else: - logger.error(f"✗ 创建物料失败: {name}, 未返回ID") - logger.error(f" 响应数据: {result}") - else: - error_msg = result.get("error") or result.get("message", "未知错误") - logger.error(f"✗ 创建物料失败: {name}") - logger.error(f" 错误信息: {error_msg}") - logger.error(f" 完整响应: {result}") - - # 避免请求过快 - time.sleep(0.3) - - logger.info(f"物料创建完成,成功创建 {len(created_materials)}/{total} 个固体物料") - return created_materials - - 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, - material_names: Optional[List[str]] = None, - type_id: str = "3a190ca0-b2f6-9aeb-8067-547e72c11469", - warehouse_name: str = "粉末加样头堆栈" - ) -> Dict[str, Any]: - """ - 传参与默认列表方式创建物料并入库(不使用CSV)。 - - Args: - material_names: 物料名称列表;默认使用 [LiPF6, LiDFOB, DTD, LiFSI, LiPO2F2] - type_id: 物料类型ID - warehouse_name: 目标仓库名(用于取位置信息) - - Returns: - 执行结果字典 - """ - logger.info("=" * 60) - logger.info(f"开始执行:从参数创建物料并批量入库到 {warehouse_name}") - logger.info("=" * 60) - - try: - # 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.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"✗ 执行失败: {e}") - return {"success": False, "error": str(e)} - - def create_material( - self, - material_name: str, - type_id: str, - warehouse_name: str, - location_name_or_id: Optional[str] = None - ) -> Dict[str, Any]: - """创建单个物料并可选入库。 - Args: - material_name: 物料名称(会优先匹配配置模板)。 - type_id: 物料类型 ID(若为空则尝试从配置推断)。 - warehouse_name: 需要入库的仓库名称;若为空则仅创建不入库。 - location_name_or_id: 具体库位名称(如 A01)或库位 UUID,由用户指定。 - Returns: - 包含创建结果、物料ID以及入库结果的字典。 - """ - material_name = (material_name or "").strip() - - resolved_type_id = (type_id or "").strip() - # 优先从 SOLID_LIQUID_MAPPINGS 中获取模板数据 - template = SOLID_LIQUID_MAPPINGS.get(material_name) - if not template: - raise ValueError(f"在配置中未找到物料 {material_name} 的模板,请检查 SOLID_LIQUID_MAPPINGS。") - material_data: Dict[str, Any] - material_data = deepcopy(template) - # 最终确保 typeId 为调用方传入的值 - if resolved_type_id: - material_data["typeId"] = resolved_type_id - material_data["name"] = material_name - # 生成唯一编码 - def _generate_code(prefix: str) -> str: - normalized = re.sub(r"\W+", "_", prefix) - normalized = normalized.strip("_") or "material" - return f"{normalized}_{datetime.now().strftime('%Y%m%d%H%M%S')}" - if not material_data.get("code"): - material_data["code"] = _generate_code(material_name) - if not material_data.get("barCode"): - material_data["barCode"] = "" - # 处理数量字段类型 - def _to_number(value: Any, default: float = 0.0) -> float: - try: - if value is None: - return default - if isinstance(value, (int, float)): - return float(value) - if isinstance(value, str) and value.strip() == "": - return default - return float(value) - except (TypeError, ValueError): - return default - material_data["quantity"] = _to_number(material_data.get("quantity"), 1.0) - material_data["warningQuantity"] = _to_number(material_data.get("warningQuantity"), 0.0) - unit = material_data.get("unit") or "个" - material_data["unit"] = unit - if not material_data.get("parameters"): - material_data["parameters"] = json.dumps({"unit": unit}, ensure_ascii=False) - # 补充子物料信息 - details = material_data.get("details") or [] - if not isinstance(details, list): - logger.warning("details 字段不是列表,已忽略。") - details = [] - else: - for idx, detail in enumerate(details, start=1): - if not isinstance(detail, dict): - continue - if not detail.get("code"): - detail["code"] = f"{material_data['code']}_{idx:02d}" - if not detail.get("name"): - detail["name"] = f"{material_name}_detail_{idx:02d}" - if not detail.get("unit"): - detail["unit"] = unit - if not detail.get("parameters"): - detail["parameters"] = json.dumps({"unit": detail.get("unit", unit)}, ensure_ascii=False) - if "quantity" in detail: - detail["quantity"] = _to_number(detail.get("quantity"), 1.0) - material_data["details"] = details - create_result = self._post_lims("/api/lims/storage/material", material_data) - # 解析创建结果中的物料 ID - material_id: Optional[str] = None - if isinstance(create_result, dict): - data_field = create_result.get("data") - if isinstance(data_field, str): - material_id = data_field - elif isinstance(data_field, dict): - material_id = data_field.get("id") or data_field.get("materialId") - inbound_result: Optional[Dict[str, Any]] = None - location_id: Optional[str] = None - # 按用户指定位置入库 - if warehouse_name and material_id and location_name_or_id: - try: - location_ids, position_names = self._load_warehouse_locations(warehouse_name) - position_to_id = {name: loc_id for name, loc_id in zip(position_names, location_ids)} - target_location_id = position_to_id.get(location_name_or_id, location_name_or_id) - if target_location_id: - location_id = target_location_id - inbound_result = self.storage_inbound(material_id, target_location_id) - else: - inbound_result = {"error": f"未找到匹配的库位: {location_name_or_id}"} - except Exception as exc: - logger.error(f"获取仓库 {warehouse_name} 位置失败: {exc}") - inbound_result = {"error": str(exc)} - return { - "success": bool(isinstance(create_result, dict) and create_result.get("code") == 1 and material_id), - "material_name": material_name, - "material_id": material_id, - "warehouse": warehouse_name, - "location_id": location_id, - "location_name_or_id": location_name_or_id, - "create_result": create_result, - "inbound_result": inbound_result, - } - def resource_tree_transfer(self, old_parent: ResourcePLR, plr_resource: ResourcePLR, parent_resource: ResourcePLR): - # ROS2DeviceNode.run_async_func(self._ros_node.resource_tree_transfer, True, **{ - # "old_parent": old_parent, - # "plr_resource": plr_resource, - # "parent_resource": parent_resource, - # }) - print("resource_tree_transfer", plr_resource, parent_resource) - if hasattr(plr_resource, "unilabos_extra") and plr_resource.unilabos_extra: - 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 MATERIAL_TYPE_MAPPINGS.items(): - if plr_model == moudle_name: - board_type = key - break - if board_type is None: - pass - bottle1 = plr_resource.children[0] - - bottle_moudle = bottle1.model - bottle_type = None - for key, (moudle_name, moudle_uuid) in MATERIAL_TYPE_MAPPINGS.items(): - if bottle_moudle == moudle_name: - bottle_type = key - break - - # 从 parent_resource 获取仓库名称 - warehouse_name = parent_resource.name if parent_resource else "手动堆栈" - logger.info(f"拖拽上料: {plr_resource.name} -> {warehouse_name} / {site}") - - self.create_sample(plr_resource.name, board_type, bottle_type, site, warehouse_name) - return - self.lab_logger().warning(f"无库位的上料,不处理,{plr_resource} 挂载到 {parent_resource}") - - def create_sample( - self, - name: str, - board_type: str, - bottle_type: str, - location_code: str, - warehouse_name: str = "手动堆栈" - ) -> Dict[str, Any]: - """创建配液板物料并自动入库。 - Args: - name: 物料名称 - board_type: 板类型,如 "5ml分液瓶板"、"配液瓶(小)板" - bottle_type: 瓶类型,如 "5ml分液瓶"、"配液瓶(小)" - location_code: 库位编号,例如 "A01" - warehouse_name: 仓库名称,默认为 "手动堆栈",支持 "自动堆栈-左"、"自动堆栈-右" 等 - """ - carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1] - bottle_type_id = MATERIAL_TYPE_MAPPINGS[bottle_type][1] - - # 从指定仓库获取库位UUID - if warehouse_name not in WAREHOUSE_MAPPING: - logger.error(f"未找到仓库: {warehouse_name},回退到手动堆栈") - warehouse_name = "手动堆栈" - - if location_code not in WAREHOUSE_MAPPING[warehouse_name]["site_uuids"]: - logger.error(f"仓库 {warehouse_name} 中未找到库位 {location_code}") - raise ValueError(f"库位 {location_code} 在仓库 {warehouse_name} 中不存在") - - location_id = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"][location_code] - logger.info(f"创建样品入库: {name} -> {warehouse_name}/{location_code} (UUID: {location_id})") - - # 新建小瓶 - details = [] - for y in range(1, 5): - for x in range(1, 3): - details.append({ - "typeId": bottle_type_id, - "code": "", - "name": str(bottle_type) + str(x) + str(y), - "quantity": "1", - "x": x, - "y": y, - "z": 1, - "unit": "个", - "parameters": json.dumps({"unit": "个"}, ensure_ascii=False), - }) - - data = { - "typeId": carrier_type_id, - "code": "", - "barCode": "", - "name": name, - "unit": "块", - "parameters": json.dumps({"unit": "块"}, ensure_ascii=False), - "quantity": "1", - "details": details, - } - # print("xxx:",data) - create_result = self._post_lims("/api/lims/storage/material", data) - sample_uuid = create_result.get("data") - - final_result = self._post_lims("/api/lims/storage/inbound", { - "materialId": sample_uuid, - "locationId": location_id, - }) - return final_result - - - - -if __name__ == "__main__": - lab_registry.setup() - deck = BIOYOND_YB_Deck(setup=True) - ws = BioyondCellWorkstation(deck=deck) - # ws.create_sample(name="test", board_type="配液瓶(小)板", bottle_type="配液瓶(小)", location_code="B01") - # logger.info(ws.scheduler_stop()) - # logger.info(ws.scheduler_start()) - - # 继续后续流程 - 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: - time.sleep(1) - # re=ws.scheduler_stop() - # re = ws.transfer_3_to_2_to_1() - - # print(re) - # 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\样品导入模板.xlsx") - # ws.wait_for_transfer_task(filter_text="物料转移任务") - # logger.info("4号箱向3号箱转运物料转移任务已完成") - - # ws.scheduler_start() - # print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items - - # # 新建实验 - # response = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092701.xlsx") - # logger.info(response) - # data_list = response.get("data", []) - # order_name = data_list[0].get("orderName", "") - - # ws.wait_for_transfer_task(filter_text=order_name) - # ws.wait_for_transfer_task(filter_text='DP20250927001') - # logger.info("3号站内实验完成") - # # ws.scheduler_start() - # # print(res) - # ws.transfer_3_to_2_to_1() - # ws.wait_for_transfer_task(filter_text="物料转移任务") - # logger.info("3号站向2号站向1号站转移任务完成") - # r321 = self.wait_for_transfer_task() - #1号站启动 - # ws.transfer_1_to_2() - # ws.wait_for_transfer_task(filter_text="物料转移任务") - # 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/bioyond_cell/bioyond_cell_workstation.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py index 5d10f40..f718fb9 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 @@ -161,6 +161,95 @@ class BioyondCellWorkstation(BioyondWorkstation): logger.warning(f"任务未知状态 ({status}) (orderCode={order_code})") return {"status": f"unknown_{status}", "report": report} + def get_material_info(self, material_id: str) -> Dict[str, Any]: + """查询物料详细信息(物料详情接口) + + Args: + material_id: 物料 ID (GUID) + + Returns: + 物料详情,包含 name, typeName, locations 等 + """ + result = self._post_lims("/api/lims/storage/material-info", material_id) + return result.get("data", {}) + + def _process_order_reagents(self, report: Dict[str, Any]) -> Dict[str, Any]: + """处理订单完成报文中的试剂数据,计算质量比 + + Args: + report: 订单完成推送的 report 数据 + + Returns: + { + "real_mass_ratio": {"试剂A": 0.6, "试剂B": 0.4}, + "target_mass_ratio": {"试剂A": 0.6, "试剂B": 0.4}, + "reagent_details": [...] # 详细数据 + } + """ + used_materials = report.get("usedMaterials", []) + + # 1. 筛选试剂(typemode="2",注意是小写且是字符串) + reagents = [m for m in used_materials if str(m.get("typemode")) == "2"] + + if not reagents: + logger.warning("订单完成报文中没有试剂(typeMode=2)") + return { + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "reagent_details": [] + } + + # 2. 查询试剂名称 + reagent_data = [] + for reagent in reagents: + material_id = reagent.get("materialId") + if not material_id: + continue + + try: + info = self.get_material_info(material_id) + name = info.get("name", f"Unknown_{material_id[:8]}") + real_qty = float(reagent.get("realQuantity", 0.0)) + used_qty = float(reagent.get("usedQuantity", 0.0)) + + reagent_data.append({ + "name": name, + "material_id": material_id, + "real_quantity": real_qty, + "used_quantity": used_qty + }) + logger.info(f"试剂: {name}, 目标={used_qty}g, 实际={real_qty}g") + except Exception as e: + logger.error(f"查询物料信息失败: {material_id}, {e}") + continue + + if not reagent_data: + return { + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "reagent_details": [] + } + + # 3. 计算质量比 + def calculate_mass_ratio(items: List[Dict], key: str) -> Dict[str, float]: + total = sum(item[key] for item in items) + if total == 0: + logger.warning(f"总质量为0,无法计算{key}质量比") + return {item["name"]: 0.0 for item in items} + return {item["name"]: round(item[key] / total, 4) for item in items} + + real_mass_ratio = calculate_mass_ratio(reagent_data, "real_quantity") + target_mass_ratio = calculate_mass_ratio(reagent_data, "used_quantity") + + logger.info(f"真实质量比: {real_mass_ratio}") + logger.info(f"目标质量比: {target_mass_ratio}") + + return { + "real_mass_ratio": real_mass_ratio, + "target_mass_ratio": target_mass_ratio, + "reagent_details": reagent_data + } + # -------------------- 基础HTTP封装 -------------------- def _url(self, path: str) -> str: @@ -643,6 +732,21 @@ class BioyondCellWorkstation(BioyondWorkstation): # 提取报文数据 if result.get("status") == "success": report = result.get("report", {}) + + # [新增] 处理试剂数据,计算质量比 + try: + mass_ratios = self._process_order_reagents(report) + report["mass_ratios"] = mass_ratios # 添加到报文中 + logger.info(f"已计算订单 {order_code} 的试剂质量比") + except Exception as e: + logger.error(f"计算试剂质量比失败: {e}") + report["mass_ratios"] = { + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "reagent_details": [], + "error": str(e) + } + all_reports.append(report) print(f"[create_orders] ✓ 订单 {order_code} 完成") else: @@ -672,6 +776,252 @@ class BioyondCellWorkstation(BioyondWorkstation): return final_result + def create_orders_v2(self, xlsx_path: str) -> Dict[str, Any]: + """ + 从 Excel 解析并创建实验(2.14)- V2版本 + 约定: + - batchId = Excel 文件名(不含扩展名) + - 物料列:所有以 "(g)" 结尾(不再读取"总质量(g)"列) + - totalMass 自动计算为所有物料质量之和 + - createTime 缺失或为空时自动填充为当前日期(YYYY/M/D) + """ + 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_v2] 使用 Excel 路径: {path}") + if path != default_path: + print("[create_orders_v2] 来源: 调用方传入自定义路径") + else: + print("[create_orders_v2] 来源: 使用默认模板路径") + + if not path.exists(): + print(f"[create_orders_v2] ⚠️ 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_v2] 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_v2] 列匹配结果:", { + "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_v2] 识别到的物料列: {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_v2] 第 {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_v2] 第 {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_v2] ⚠️ 第 {idx+1} 行总质量 <= 0,可能导致 LIMS 校验失败") + if not mats: + 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}") + + # 提取所有返回的 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) + + if not order_codes: + logger.error("未找到任何有效的 orderCode!") + return response + + print(f"[create_orders_v2] 等待 {len(order_codes)} 个订单完成: {order_codes}") + + # ========== 步骤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({ + "orderCode": order_code, + "status": result.get("status"), + "error": result.get("message", "未知错误") + }) + + print(f"[create_orders_v2] 所有订单已完成,共收集 {len(all_reports)} 个报文") + + # ========== 步骤2: 统一计算所有订单的质量比 ========== + print(f"[create_orders_v2] 开始统一计算 {len(all_reports)} 个订单的质量比...") + all_mass_ratios = [] # 存储所有订单的质量比,与reports顺序一致 + + 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", {}) + }) + logger.info(f"✓ 已计算订单 {order_code} 的试剂质量比") + except Exception as e: + 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": {}, + "error": str(e) + }) + else: + # 失败的订单不计算质量比 + all_mass_ratios.append({ + "orderCode": order_code, + "orderName": report.get("orderName", "N/A"), + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "error": "订单未成功完成" + }) + + print(f"[create_orders_v2] 质量比计算完成") + print("实验记录本========================create_orders_v2========================") + + # 返回所有订单的完成报文 + 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 + } + + 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("========================") + + return final_result + # 2.7 启动调度 def scheduler_start(self) -> Dict[str, Any]: return self._post_lims("/api/lims/scheduler/start") @@ -683,7 +1033,7 @@ class BioyondCellWorkstation(BioyondWorkstation): 请求体只包含 apiKey 和 requestTime """ return self._post_lims("/api/lims/scheduler/stop") - # 2.9 继续调度 + # 2.9 继续调度 def scheduler_continue(self) -> Dict[str, Any]: """ @@ -867,6 +1217,48 @@ class BioyondCellWorkstation(BioyondWorkstation): result = self.wait_for_order_finish(order_code) return result + def transfer_3_to_2(self, + 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]: + """ + 2.34 3-2 物料转运接口 + + 新建从 3 -> 2 的搬运任务 + + Args: + source_wh_id: 来源仓库 Id (默认为3号仓库) + source_x: 来源位置 X 坐标 + source_y: 来源位置 Y 坐标 + source_z: 来源位置 Z 坐标 + + Returns: + dict: 包含任务 orderId 和 orderCode 的响应 + """ + payload: Dict[str, Any] = { + "sourcePosX": source_x, + "sourcePosY": source_y, + "sourcePosZ": source_z + } + if source_wh_id: + payload["sourceWHID"] = source_wh_id + + logger.info(f"[transfer_3_to_2] 开始转运: 仓库={source_wh_id}, 位置=({source_x}, {source_y}, {source_z})") + response = self._post_lims("/api/lims/order/transfer-task3To2", payload) + + # 等待任务报送成功 + order_code = response.get("data", {}).get("orderCode") + if not order_code: + logger.error("[transfer_3_to_2] 转运任务未返回有效 orderCode!") + return response + + logger.info(f"[transfer_3_to_2] 转运任务已创建: {order_code}") + # 等待完成报送 + result = self.wait_for_order_finish(order_code) + logger.info(f"[transfer_3_to_2] 转运任务完成: {order_code}") + return result + # 3.35 1→2 物料转运 def transfer_1_to_2(self) -> Dict[str, Any]: """ @@ -874,14 +1266,28 @@ class BioyondCellWorkstation(BioyondWorkstation): URL: /api/lims/order/transfer-task1To2 只需要 apiKey 和 requestTime """ + logger.info("[transfer_1_to_2] 开始 1→2 物料转运") response = self._post_lims("/api/lims/order/transfer-task1To2") - # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") + logger.info(f"[transfer_1_to_2] API Response: {response}") + + # 等待任务报送成功 - 处理不同的响应格式 + order_code = None + data_field = response.get("data") + + if isinstance(data_field, dict): + order_code = data_field.get("orderCode") + elif isinstance(data_field, str): + # 某些接口可能直接返回 orderCode 字符串 + order_code = data_field + if not order_code: - logger.error("上料任务未返回有效 orderCode!") + logger.error(f"[transfer_1_to_2] 转运任务未返回有效 orderCode!响应: {response}") return response - # 等待完成报送 + + logger.info(f"[transfer_1_to_2] 转运任务已创建: {order_code}") + # 等待完成报送 result = self.wait_for_order_finish(order_code) + logger.info(f"[transfer_1_to_2] 转运任务完成: {order_code}") return result # 2.5 批量查询实验报告(post过滤关键字查询) diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx index 1b0eb9b..88b233d 100644 Binary files a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/smiles&molweight.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/smiles&molweight.py new file mode 100644 index 0000000..195e87a --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/smiles&molweight.py @@ -0,0 +1,12 @@ +import pubchempy as pcp + +cas = "21324-40-3" # 示例 +comps = pcp.get_compounds(cas, namespace="name") +if not comps: + raise ValueError("No hit") + +c = comps[0] + +print("Canonical SMILES:", c.canonical_smiles) +print("Isomeric SMILES:", c.isomeric_smiles) +print("MW:", c.molecular_weight) diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py index 9a70d5c..ec1c90d 100644 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ b/unilabos/devices/workstation/bioyond_studio/config.py @@ -133,6 +133,46 @@ WAREHOUSE_MAPPING = { "J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9" } }, + "手动传递窗右": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", + "A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe", + "A03": "3a19deae-2c7a-5876-c454-6b7e224ca927", + "B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a", + "C01": "3a19deae-2c7a-32bc-768e-556647e292f3", + "C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4", + "C03": "3a19deae-2c7a-3056-6504-10dc73fbc276", + "D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440", + "D02": "3a19deae-2c7a-61be-601c-b6fb5610499a", + "D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560", + "E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363", + "E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910", + "E03": "3a19deae-2c7a-b163-2219-23df15200311", + } + }, + "手动传递窗左": { + "uuid": "", + "site_uuids": { + "F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a", + "F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679", + "F03": "3a19deae-2c7a-f7c4-12bd-425799425698", + "G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955", + "G02": "3a19deae-2c7a-204e-95ed-1f1950f28343", + "G03": "3a19deae-2c7a-392b-62f1-4907c66343f8", + "H01": "3a19deae-2c7a-5602-e876-d27aca4e3201", + "H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702", + "H03": "3a19deae-2c7a-780b-8965-2e1345f7e834", + "I01": "3a19deae-2c7a-8849-e172-07de14ede928", + "I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0", + "I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4", + "J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6", + "J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205", + "J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9" + } + }, "4号手套箱内部堆栈": { "uuid": "", "site_uuids": { 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 f47f560..34f11e9 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -20,8 +20,7 @@ from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck from unilabos.resources.graphio import convert_resources_to_type from unilabos.utils.log import logger -from pymodbus.payload import BinaryPayloadDecoder -from pymodbus.constants import Endian +import struct def _decode_float32_correct(registers): @@ -43,13 +42,19 @@ def _decode_float32_correct(registers): return 0.0 try: - # 使用正确的字节序配置 - decoder = BinaryPayloadDecoder.fromRegisters( - registers, - byteorder=Endian.Big, # 字节序始终为Big - wordorder=Endian.Little # 字序为Little (根据PLC配置) - ) - return decoder.decode_32bit_float() + # Word Order: Little - 交换两个寄存器的顺序 + # Byte Order: Big - 每个寄存器内部使用大端字节序 + low_word = registers[0] + high_word = registers[1] + + # 将两个16位寄存器组合成一个32位值 (Little Word Order) + # 使用大端字节序 ('>') 将每个寄存器转换为字节 + byte_string = struct.pack('>HH', high_word, low_word) + + # 将字节字符串解码为浮点数 (大端) + value = struct.unpack('>f', byte_string)[0] + + return value except Exception as e: logger.error(f"解码FLOAT32失败: {e}, registers: {registers}") return 0.0 @@ -632,23 +637,22 @@ class CoinCellAssemblyWorkstation(WorkstationBase): # 读取 STRING 类型数据 code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE) - # 处理 bytes 或 string 类型 - if isinstance(code_little, bytes): - code_str = code_little.decode('utf-8', errors='ignore') - elif isinstance(code_little, str): - code_str = code_little - else: - logger.warning(f"电池二维码返回的类型不支持: {type(code_little)}") + # PyModbus 3.x 返回 string 类型 + if not isinstance(code_little, str): + logger.warning(f"电池二维码返回的类型不支持: {type(code_little)}, 值: {repr(code_little)}") return "N/A" - # 取前8个字符 - raw_code = code_str[:8] + # 从字符串末尾查找连续的字母数字字符(反转字符串) + import re + reversed_str = code_little[::-1] + match = re.match(r'^([A-Za-z0-9]+)', reversed_str) - # LITTLE字节序需要每2个字符交换位置 - clean_code = ''.join([raw_code[i+1] + raw_code[i] for i in range(0, len(raw_code), 2)]) + if not match: + logger.warning(f"未找到有效的电池二维码数据,原始字符串: {repr(code_little)}") + return "N/A" - # 去除空字符和空格 - decoded = clean_code.replace('\x00', '').replace('\r', '').replace('\n', '').strip() + # 提取匹配到的字符串(已经是正确顺序) + decoded = match.group(1)[:8] # 只取前8个字符 return decoded if decoded else "N/A" except Exception as e: @@ -663,23 +667,22 @@ class CoinCellAssemblyWorkstation(WorkstationBase): # 读取 STRING 类型数据 code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE) - # 处理 bytes 或 string 类型 - if isinstance(code_little, bytes): - code_str = code_little.decode('utf-8', errors='ignore') - elif isinstance(code_little, str): - code_str = code_little - else: - logger.warning(f"电解液二维码返回的类型不支持: {type(code_little)}") + # PyModbus 3.x 返回 string 类型 + if not isinstance(code_little, str): + logger.warning(f"电解液二维码返回的类型不支持: {type(code_little)}, 值: {repr(code_little)}") return "N/A" - # 取前8个字符 - raw_code = code_str[:8] + # 从字符串末尾查找连续的字母数字字符(反转字符串) + import re + reversed_str = code_little[::-1] + match = re.match(r'^([A-Za-z0-9]+)', reversed_str) - # LITTLE字节序需要每2个字符交换位置 - clean_code = ''.join([raw_code[i+1] + raw_code[i] for i in range(0, len(raw_code), 2)]) + if not match: + logger.warning(f"未找到有效的电解液二维码数据,原始字符串: {repr(code_little)}") + return "N/A" - # 去除空字符和空格 - decoded = clean_code.replace('\x00', '').replace('\r', '').replace('\n', '').strip() + # 提取匹配到的字符串(已经是正确顺序) + decoded = match.group(1)[:8] # 只取前8个字符 return decoded if decoded else "N/A" except Exception as e: @@ -940,6 +943,111 @@ class CoinCellAssemblyWorkstation(WorkstationBase): time.sleep(1) #自动按钮置False + def func_sendbottle_allpack_multi( + self, + elec_num, + elec_use_num, + elec_vol: int = 50, + # 电解液双滴模式参数 + dual_drop_mode: bool = False, + dual_drop_first_volume: int = 25, + dual_drop_suction_timing: bool = False, + dual_drop_start_timing: bool = False, + assembly_type: int = 7, + assembly_pressure: int = 4200, + # 来自原 qiming_coin_cell_code 的参数 + fujipian_panshu: int = 0, + fujipian_juzhendianwei: int = 0, + gemopanshu: int = 0, + gemo_juzhendianwei: int = 0, + qiangtou_juzhendianwei: int = 0, + lvbodian: bool = True, + battery_pressure_mode: bool = True, + battery_clean_ignore: bool = False, + file_path: str = "/Users/sml/work" + ) -> Dict[str, Any]: + """ + 发送瓶数+简化组装函数(适用于第二批次及后续批次) + + 合并了发送瓶数和简化组装流程,用于连续批次生产。 + 适用场景:设备已完成初始化和启动,仍在自动模式下运行。 + + Args: + elec_num: 电解液瓶数 + elec_use_num: 每瓶电解液组装的电池数 + elec_vol: 电解液吸液量 (μL) + dual_drop_mode: 电解液添加模式 (False=单次滴液, True=二次滴液) + dual_drop_first_volume: 二次滴液第一次排液体积 (μL) + dual_drop_suction_timing: 二次滴液吸液时机 (False=正常吸液, True=先吸液) + dual_drop_start_timing: 二次滴液开始滴液时机 (False=正极片前, True=正极片后) + assembly_type: 组装类型 (7=不用铝箔垫, 8=使用铝箔垫) + assembly_pressure: 电池压制力 (N) + fujipian_panshu: 负极片盘数 + fujipian_juzhendianwei: 负极片矩阵点位 + gemopanshu: 隔膜盘数 + gemo_juzhendianwei: 隔膜矩阵点位 + qiangtou_juzhendianwei: 枪头盒矩阵点位 + lvbodian: 是否使用铝箔垫片 + battery_pressure_mode: 是否启用压力模式 + battery_clean_ignore: 是否忽略电池清洁 + file_path: 实验记录保存路径 + + Returns: + dict: 包含组装结果的字典 + + 注意: + - 第一次启动需先调用 func_pack_device_init_auto_start_combined() + - 后续批次直接调用此函数即可 + """ + logger.info("=" * 60) + logger.info("开始发送瓶数+简化组装流程...") + logger.info(f"电解液瓶数: {elec_num}, 每瓶电池数: {elec_use_num}") + logger.info("=" * 60) + + # 步骤1: 发送电解液瓶数(触发物料搬运) + logger.info("步骤1/2: 发送电解液瓶数,触发物料搬运...") + try: + self.func_pack_send_bottle_num(elec_num) + logger.info("✓ 瓶数发送完成,物料搬运中...") + except Exception as e: + logger.error(f"发送瓶数失败: {e}") + return { + "success": False, + "error": f"发送瓶数失败: {e}", + "total_batteries": 0, + "batteries": [] + } + + # 步骤2: 执行简化组装流程 + logger.info("步骤2/2: 开始简化组装流程...") + result = self.func_allpack_cmd_simp( + elec_num=elec_num, + elec_use_num=elec_use_num, + elec_vol=elec_vol, + dual_drop_mode=dual_drop_mode, + dual_drop_first_volume=dual_drop_first_volume, + dual_drop_suction_timing=dual_drop_suction_timing, + dual_drop_start_timing=dual_drop_start_timing, + assembly_type=assembly_type, + assembly_pressure=assembly_pressure, + fujipian_panshu=fujipian_panshu, + fujipian_juzhendianwei=fujipian_juzhendianwei, + gemopanshu=gemopanshu, + gemo_juzhendianwei=gemo_juzhendianwei, + qiangtou_juzhendianwei=qiangtou_juzhendianwei, + lvbodian=lvbodian, + battery_pressure_mode=battery_pressure_mode, + battery_clean_ignore=battery_clean_ignore, + file_path=file_path + ) + + logger.info("=" * 60) + logger.info("发送瓶数+简化组装流程完成") + logger.info(f"总组装电池数: {result.get('total_batteries', 0)}") + logger.info("=" * 60) + + return result + # 下发参数 #def func_pack_send_msg_cmd(self, elec_num: int, elec_use_num: int, elec_vol: float, assembly_type: int, assembly_pressure: int) -> bool: diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1105.csv b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1105.csv deleted file mode 100644 index 3f7b357..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1105.csv +++ /dev/null @@ -1,64 +0,0 @@ -Name,DataType,InitValue,Comment,Attribute,DeviceType,Address, -COIL_SYS_START_CMD,BOOL,,,,coil,9010, -COIL_SYS_STOP_CMD,BOOL,,,,coil,9020, -COIL_SYS_RESET_CMD,BOOL,,,,coil,9030, -COIL_SYS_HAND_CMD,BOOL,,,,coil,9040, -COIL_SYS_AUTO_CMD,BOOL,,,,coil,9050, -COIL_SYS_INIT_CMD,BOOL,,,,coil,9060, -COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,9700, -COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,9710,unilab_rec_msg_succ_cmd -COIL_SYS_START_STATUS,BOOL,,,,coil,9210, -COIL_SYS_STOP_STATUS,BOOL,,,,coil,9220, -COIL_SYS_RESET_STATUS,BOOL,,,,coil,9230, -COIL_SYS_HAND_STATUS,BOOL,,,,coil,9240, -COIL_SYS_AUTO_STATUS,BOOL,,,,coil,9250, -COIL_SYS_INIT_STATUS,BOOL,,,,coil,9260, -COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,9500, -COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,9510,request_send_msg_status -REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,17000, -REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,17002,unilab_send_msg_electrolyte_num -REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,17004,unilab_send_msg_electrolyte_vol -REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,17006,unilab_send_msg_assembly_type -REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,17008,unilab_send_msg_assembly_pressure -REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,16000,data_assembly_coin_cell_num -REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,16002,data_open_circuit_voltage -REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,16004, -REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,16006, -REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,16008, -REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,16010,data_pole_weight -REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,16012,data_assembly_time -REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,16014,data_assembly_pressure -REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,16016,data_electrolyte_volume -REG_DATA_COIN_NUM,INT16,,,,hold_register,16018,data_coin_num -REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,16020,data_electrolyte_code() -REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,16030,data_coin_cell_code() -REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,18004,data_stack_vision_code() -REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,16050,data_glove_box_pressure -REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,16052,data_glove_box_water_content -REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,16054,data_glove_box_o2_content -UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9720, -UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9520, -REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,17496, -REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,16000, -UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,9730, -UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,9530, -REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,16018,ASSEMBLY_TYPE7or8 -COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,9340, -REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,17440, -REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,17450, -REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,17480, -REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,17443, -REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,17453, -REG_MSG_PRESS_MODE,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,9360,电池压制模式 -,,,,,,, -,BOOL,,视觉对位(false:使用,true:忽略),,coil,9300,视觉对位 -,BOOL,,复检(false:使用,true:忽略),,coil,9310,视觉复检 -,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,9320,手套箱左仓 -,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,9420,手套箱右仓 -,BOOL,,真空检知(false:使用,true:忽略),,coil,9350,真空检知 -,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,9370,滴液模式 -,BOOL,,正极片称重(false:使用,true:忽略),,coil,9380,正极片称重 -,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,9390,正负极反装 -,BOOL,,压制清洁(false:使用,true:忽略),,coil,9400,压制清洁 -,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,9410,负极片摆盘方式 -REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁(false:使用,true:忽略),,coil,9460, diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1223.csv b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1223.csv deleted file mode 100644 index 9212f9b..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_1223.csv +++ /dev/null @@ -1,159 +0,0 @@ -Name,DataType,Comment,DeviceType,Address,, -COIL_SYS_START_CMD,BOOL,豸,coil,8010,, -COIL_SYS_STOP_CMD,BOOL,豸ֹͣ,coil,8020,, -COIL_SYS_RESET_CMD,BOOL,豸λ,coil,8030,, -COIL_SYS_HAND_CMD,BOOL,豸ֶģʽ,coil,8040,, -COIL_SYS_AUTO_CMD,BOOL,豸Զģʽ,coil,8050,, -COIL_SYS_INIT_CMD,BOOL,豸ʼģʽ,coil,8060,, -COIL_SYS_STOP_STATUS,BOOL,豸ͣ,coil,8220,, -,,,,,, -,BOOL,UNILAB͵Һƿ,coil,8720,, -,BOOL,豸ܵҺƿ,coil,8520,, -REG_MSG_ELECTROLYTE_NUM,WORD,Һʹƿ,hold_register,496,, -,WORD,Ƭ̾λʼλ0,hold_register,440,, -,WORD,Ĥ̾λʼλ0,hold_register,450,, -,WORD,Һƿ_Ͼλʼλ0,hold_register,460,, -,WORD,Һƿ_վλʼλ0,hold_register,430,, -,WORD,g_Һƿ_޸˸׾λʼλ0,hold_register,470,, -,WORD,Һǹͷλʼλ0,hold_register,480,, -,WORD,øƬ,hold_register,443,, -,WORD,øĤ,hold_register,453,, -,,,,,, -COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,UNILAB䷽,coil,8700,, -COIL_REQUEST_REC_MSG_STATUS,BOOL,豸䷽,coil,8500,, -REG_MSG_ELECTROLYTE_USE_NUM,INT16,ƿҺʹô,hold_register,11000,, -REG_MSG_ELECTROLYTE_VOLUME,INT16,Һȡ,hold_register,11004,, -REG_MSG_ASSEMBLY_PRESSURE,INT16,װѹ,hold_register,11008,, -REG_DATA_ELECTROLYTE_CODE,STRING,Һάк,hold_register,10020,, -,BOOL,Ӿλfalse:ʹãtrue:ԣ,coil,8300,, -,BOOL,죨false:ʹãtrue:ԣ,coil,8310,, -,BOOL,_֣false:ʹãtrue:ԣ,coil,8320,, -,BOOL,_Ҳ֣false:ʹãtrue:ԣ,coil,8420,, -,BOOL,еְ̣false:ʹãtrue:ԣ,coil,8330,, -,BOOL,ϣfalse:ʹãtrue:ԣ,coil,8340,, -,BOOL,ռ֪false:ʹãtrue:ԣ,coil,8350,, -,BOOL,ѹģʽfalse:ѹģʽTrue:ģʽ,coil,8360,, -,BOOL,Һģʽfalse:εҺtrue:εҺ,coil,8370,, -,BOOL,Ƭأfalse:ʹãtrue:ԣ,coil,8380,, -,BOOL,Ƭװʽfalse:װtrue:װ,coil,8390,, -,BOOL,ѹࣨfalse:ʹãtrue:ԣ,coil,8400,, -,BOOL,̷̰ʽfalse:ˮƽ̣true:ѵ̣,coil,8410,, -COIL_SYS_UNILAB_INTERACT ,BOOL,Unilabfalse:ʹãtrue:ԣ,coil,8450,, -,BOOL,Եࣨfalse:ʹãtrue:ԣ,colil,8460,, -,,,,,, -COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,UNILAB䷽,coil,8510,, -COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,UNILABܲ,coil,8710,, -REG_DATA_POLE_WEIGHT,FLOAT32,ǰƬ,hold_register,10010,, -REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,ǰŵװʱ,hold_register,10012,, -REG_DATA_ASSEMBLY_PRESSURE,INT16,ǰװѹ,hold_register,10014,, -REG_DATA_ELECTROLYTE_VOLUME,INT16,ǰҺע,hold_register,10016,, -REG_DATA_ASSEMBLY_TYPE,INT16,װƬѵʽ(7/8),hold_register,10018,, -REG_DATA_ELECTROLYTE_CODE,STRING,Һάк,hold_register,10020,, -REG_DATA_COIN_CELL_CODE,STRING,ضάк,hold_register,10030,, -REG_DATA_STACK_VISON_CODE,STRING,϶ѵͼƬ,hold_register,10040,, -REG_DATA_ELECTROLYTE_USE_NUM,INT16,ǰ缫ҺװR,hold_register,10000,, -REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,ǰصѹ,hold_register,10002,, -,INT,еȡϼĴ1-ǡ2-桢3-Ƭ4-Ĥ5-Ƭ6-ƽ桢7-桢8-ǣ,hold_register,10060,, -,,,,,, -,INT,ǰƬʣR,hold_register,10062,PLCַ,1223- -,INT,ǰĤR,hold_register,10064,, -,INT,Һ״̬루R,hold_register,10066,, -,REAL,·ѹOKֵR,hold_register,10068,, -,REAL,·ѹOKֵR,hold_register,10070,, -,INT,ǰװR,hold_register,10072,, -,INT,ǰװR,hold_register,10074,, -,REAL,10mmƬʣR,hold_register,520,HMIַ, -,REAL,12mmƬʣR,hold_register,522,, -,REAL,16mmƬʣR,hold_register,524,, -,REAL,ʣR,hold_register,526,, -,REAL,ʣR,hold_register,528,, -,REAL,ƽʣR,hold_register,530,, -,REAL,ʣR,hold_register,532,, -,REAL,ʣR,hold_register,534,, -,REAL,ƷʣR,hold_register,536,, -,REAL,ƷNGʣR,hold_register,538,, -,,,,,, -,REAL,10mmƬȣW,hold_register,540,, -,REAL,12mmƬȣW,hold_register,542,, -,REAL,16mmƬȣW,hold_register,544,, -,REAL,ȣW,hold_register,546,, -,REAL,ǺȣW,hold_register,548,, -,REAL,ƽȣW,hold_register,550,, -,REAL,øǺȣW,hold_register,552,, -,REAL,õȣW,hold_register,554,, -,REAL,óƷغȣW,hold_register,556,, -,,,,,, -,,,,,, -REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,ѹ,hold_register,10050,, -REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,ˮ,hold_register,10052,, -REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,hold_register,10054,, -,,,,,, -,BOOL,쳣100-ϵͳ쳣,coil,1000,, -,BOOL,쳣101-ͣ,coil,1010,, -,BOOL,쳣111-伱ͣ,coil,1110,, -,BOOL,쳣112-ڹդڵ,coil,1120,, -,BOOL,쳣160-Һǹͷȱ,coil,1600,, -,BOOL,쳣161-ȱ,coil,1610,, -,BOOL,쳣162-ȱ,coil,1620,, -,BOOL,쳣163-Ƭȱ,coil,1630,, -,BOOL,쳣164-Ĥȱ,coil,1640,, -,BOOL,쳣165-Ƭȱ,coil,1650,, -,BOOL,쳣166-ƽȱ,coil,1660,, -,BOOL,쳣167-ȱ,coil,1670,, -,BOOL,쳣168-ȱ,coil,1680,, -,BOOL,쳣169-Ʒ,coil,1690,, -,BOOL,쳣201-ŷ01쳣,coil,2010,, -,BOOL,쳣202-ŷ02쳣,coil,2020,, -,BOOL,쳣203-ŷ03쳣,coil,2030,, -,BOOL,쳣204-ŷ04쳣,coil,2040,, -,BOOL,쳣205-ŷ05쳣,coil,2050,, -,BOOL,쳣206-ŷ06쳣,coil,2060,, -,BOOL,쳣207-ŷ07쳣,coil,2070,, -,BOOL,쳣208-ŷ08쳣,coil,2080,, -,BOOL,쳣209-ŷ09쳣,coil,2090,, -,BOOL,쳣210-ŷ10쳣,coil,2100,, -,BOOL,쳣211-ŷ11쳣,coil,2110,, -,BOOL,쳣212-ŷ12쳣,coil,2120,, -,BOOL,쳣213-ŷ13쳣,coil,2130,, -,BOOL,쳣214-ŷ14쳣,coil,2140,, -,BOOL,쳣250-Ԫ쳣,coil,2500,, -,BOOL,쳣251-ҺǹͨѶ쳣,coil,2510,, -,BOOL,쳣252-Һǹ,coil,2520,, -,BOOL,쳣256-צ쳣,coil,2560,, -,BOOL,쳣262-RBδ֪λ,coil,2620,, -,BOOL,쳣263-RBXYZ,coil,2630,, -,BOOL,쳣264-RBӾ,coil,2640,, -,BOOL,쳣265-RB1#ȡʧ,coil,2650,, -,BOOL,쳣266-RB2#ȡʧ,coil,2660,, -,BOOL,쳣267-RB3#ȡʧ,coil,2670,, -,BOOL,쳣268-RB4#ȡʧ,coil,2680,, -,BOOL,쳣269-RBȡʧ,coil,2690,, -,BOOL,쳣280-RBײ쳣,coil,2800,, -,BOOL,쳣290-ӾϵͳͨѶ쳣,coil,2900,, -,BOOL,쳣291-ӾλNG쳣,coil,2910,, -,BOOL,쳣292-ɨǹͨѶ쳣,coil,2920,, -,BOOL,쳣310-쳣,coil,3100,, -,BOOL,쳣311-쳣,coil,3110,, -,BOOL,쳣312-쳣,coil,3120,, -,BOOL,쳣313-쳣,coil,3130,, -,BOOL,쳣340-·ѹ쳣,coil,3400,, -,BOOL,쳣342-·ѹ쳣,coil,3420,, -,BOOL,쳣344-·ѹѹ쳣,coil,3440,, -,BOOL,쳣350-쳣,coil,3500,, -,BOOL,쳣352-쳣,coil,3520,, -,BOOL,쳣354-ϴ޳쳣,coil,3540,, -,BOOL,쳣356-ϴ޳ѹ쳣,coil,3560,, -,BOOL,쳣360-Һƿλ쳣,coil,3600,, -,BOOL,쳣362-Һǹͷжλ쳣,coil,3620,, -COIL ALARM_364_SERVO_DRIVE_ERROR,BOOL,쳣364-Լƿצ쳣,coil,3640,, -COIL ALARM_367_SERVO_DRIVER_ERROR,BOOL,쳣366-Լƿצ쳣,coil,3660,, -COIL ALARM_370_SERVO_MODULE_ERROR,BOOL,쳣370-ѹģ鴵쳣,coil,3700,, -,,,,,, -,,,,,, -,,,,,, -,,,,,, -,,,,,, -,,,,,, -,,,,,, -COIL + ģ/ + д+»߷ָ--boolֵ,,,,,, -REG + ģ/ + д+»߷ָ--ԼĴ,,,,,, diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20251224.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20251224.csv deleted file mode 100644 index 8a8c176..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20251224.csv +++ /dev/null @@ -1,2 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20251224_172304,-5.537573695435827e-37,-48.45097351074219,1.372190511464448e+16,3820,30,7,b'\x00\x00d\x00eaoR',b'\x00\x00\x01\x00\x00\x00\r\n' diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20251225.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20251225.csv deleted file mode 100644 index d51070e..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20251225.csv +++ /dev/null @@ -1,2 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20251225_105600,5.566961054206384e-37,-53149746331648.0,3271557120.0,3658,10,7,b'\x00\x00d\x00eaoR',b'\x00\x00\x01\x00\x00\x00\r\n' diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20251229.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20251229.csv deleted file mode 100644 index 124bff1..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20251229.csv +++ /dev/null @@ -1,2 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20251229_161836,-5.537573695435827e-37,8.919000478163591e+20,-3.806253867691382e-29,3544,20,7,b'\x00\x00d\x00eaoR',b'\x00\x00\x01\x00\x00\x00\r\n' diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20251230.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20251230.csv deleted file mode 100644 index 7fefb59..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20251230.csv +++ /dev/null @@ -1,9 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20251230_182319,0.01600000075995922,13.899999618530273,175.0,3836,20,7,b'\x00\x00d\x00eaoR',b'\x00\x00\x01\x00\x00\x00\r\n' -20251230_185306,0.01600000075995922,13.639999389648438,625.0,3819,20,7,deaoR, -20251230_192124,0.0,8.949999809265137,414.0,3803,20,8,deaoR, -20251230_195621,3.8359999656677246,10.069999694824219,205.0,3350,20,8,LG600001,19311909 -20251230_200830,0.7929999828338623,9.34999942779541,18.0,3318,20,8,LG600001,19533419 -20251230_201123,0.0,9.169999122619629,17.0,3269,20,8,LG600001,20054389 -20251230_201410,0.0,9.569999694824219,18.0,3237,20,8,LG600001,YS102704 -20251230_201659,0.0,9.699999809265137,169.0,3318,20,8,LG600001,20112754 diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260106.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260106.csv deleted file mode 100644 index f9cde47..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/date_20260106.csv +++ /dev/null @@ -1,3 +0,0 @@ -Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code -20260106_221708,0.03200000151991844,26.26999855041504,18.0,3803,30,7,NoRead88,22000063 -20260106_221957,0.11299999803304672,26.26999855041504,170.0,3787,30,7,LG600001,22124813 diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260110.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260110.csv new file mode 100644 index 0000000..de333a0 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260110.csv @@ -0,0 +1,6 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code +20260110_153356,0.0,23.889999389648438,17.0,3609,40,7,deaoR, +20260110_153640,3.430999994277954,23.719999313354492,162.0,3560,40,7,deaoR, +20260110_162905,0.0,23.920000076293945,772.0,3625,30,7,LG600001,YS103130 +20260110_163526,3.430999994277954,24.010000228881836,234.0,3690,30,7,LG600001,YS102964 +20260110_164530,0.0,23.589998245239258,219.0,3690,30,7,LG600001,YS102857 diff --git a/unilabos/devices/workstation/coin_cell_assembly/interactive_battery_export_demo.py b/unilabos/devices/workstation/coin_cell_assembly/interactive_battery_export_demo.py deleted file mode 100644 index adf2082..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/interactive_battery_export_demo.py +++ /dev/null @@ -1,536 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""扣式电池组装系统 - 交互式CSV导出演示脚本(增强版) - -此脚本专为交互式使用优化,提供清洁的命令行界面, -禁用了所有调试信息输出,确保用户可以顺畅地输入命令。 - -主要功能: -1. 手动导出设备数据到CSV文件(包含6个关键数据字段) -2. 查看CSV文件内容和导出状态 -3. 兼容原有的电池组装完成状态自动导出功能 -4. 实时查看设备数据和电池数量 - -数据字段: -- timestamp: 时间戳 -- assembly_time: 单颗电池组装时间(秒) -- open_circuit_voltage: 开路电压值(V) -- pole_weight: 正极片称重数据(g) -- battery_qr_code: 电池二维码序列号 -- electrolyte_qr_code: 电解液二维码序列号 - -使用方法: -1. 确保设备已连接并可正常通信 -2. 运行此脚本: python interactive_battery_export_demo.py -3. 使用交互式命令控制导出功能 -""" - -import time -import os -import sys -import logging -import csv -from datetime import datetime -from pathlib import Path - -# 完全禁用所有调试和信息级别的日志输出 -logging.getLogger().setLevel(logging.CRITICAL) -logging.getLogger('pymodbus').setLevel(logging.CRITICAL) -logging.getLogger('unilabos').setLevel(logging.CRITICAL) -logging.getLogger('pymodbus.logging').setLevel(logging.CRITICAL) -logging.getLogger('pymodbus.logging.tcp').setLevel(logging.CRITICAL) -logging.getLogger('pymodbus.logging.base').setLevel(logging.CRITICAL) -logging.getLogger('pymodbus.logging.decoders').setLevel(logging.CRITICAL) - -# 添加当前目录到Python路径,以便正确导入模块 -current_dir = Path(__file__).parent -sys.path.insert(0, str(current_dir.parent.parent.parent)) # 添加unilabos根目录 -sys.path.insert(0, str(current_dir)) # 添加当前目录 - -# 导入扣式电池组装系统 -try: - from unilabos.devices.coin_cell_assembly.coin_cell_assembly_system import Coin_Cell_Assembly -except ImportError: - # 如果上述导入失败,尝试直接导入 - try: - from coin_cell_assembly_system import Coin_Cell_Assembly - except ImportError as e: - print(f"导入错误: {e}") - print("请确保在正确的目录下运行此脚本,或者将unilabos添加到Python路径中") - sys.exit(1) - -def clear_screen(): - """清屏函数""" - os.system('cls' if os.name == 'nt' else 'clear') - -def print_header(): - """打印程序头部信息""" - print("="*60) - print(" 扣式电池组装系统 - 交互式CSV导出控制台") - print("="*60) - print() - -def print_commands(): - """打印可用命令""" - print("可用命令:") - print(" start - 启动电池组装完成状态导出") - print(" stop - 停止导出") - print(" status - 查看导出状态") - print(" data - 查看当前设备数据") - print(" count - 查看当前电池数量") - print(" export - 手动导出当前数据到CSV") - print(" setpath - 设置自定义CSV文件路径") - print(" view - 查看CSV文件内容") - print(" force - 强制继续CSV导出(即使设备停止)") - print(" detail - 显示详细设备状态") - print(" clear - 清屏") - print(" help - 显示帮助信息") - print(" quit - 退出程序") - print("-"*60) - -def print_status_info(device, csv_file_path): - """打印状态信息""" - try: - status = device.get_csv_export_status() - is_running = status.get('running', False) - export_file = status.get('file_path', None) - thread_alive = status.get('thread_alive', False) - device_status = status.get('device_status', 'N/A') - battery_count = status.get('battery_count', 'N/A') - - print(f"导出状态: {'运行中' if is_running else '已停止'}") - print(f"导出文件: {export_file if export_file else 'N/A'}") - print(f"线程状态: {'活跃' if thread_alive else '非活跃'}") - print(f"设备状态: {device_status}") - print(f"电池计数: {battery_count}") - - # 检查手动导出的CSV文件 - if os.path.exists(csv_file_path): - file_size = os.path.getsize(csv_file_path) - print(f"手动导出文件: {csv_file_path} ({file_size} 字节)") - else: - print(f"手动导出文件: {csv_file_path} (不存在)") - - # 显示设备运行状态 - try: - print("\n=== 设备运行状态 ===") - print(f"系统启动状态: {device.sys_start_status}") - print(f"系统停止状态: {device.sys_stop_status}") - print(f"自动模式状态: {device.sys_auto_status}") - print(f"手动模式状态: {device.sys_hand_status}") - except Exception as e: - print(f"获取设备运行状态失败: {e}") - - except Exception as e: - print(f"获取状态失败: {e}") - -def collect_device_data(device): - """收集设备的六个关键数据""" - try: - timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - # 读取各项数据,添加错误处理和重试机制 - try: - assembly_time = device.data_assembly_time # 单颗电池组装时间(秒) - # 确保返回的是数值类型 - if isinstance(assembly_time, (list, tuple)) and len(assembly_time) > 0: - assembly_time = float(assembly_time[0]) - else: - assembly_time = float(assembly_time) - except Exception as e: - print(f"读取组装时间失败: {e}") - assembly_time = 0.0 - - try: - open_circuit_voltage = device.data_open_circuit_voltage # 开路电压值(V) - # 确保返回的是数值类型 - if isinstance(open_circuit_voltage, (list, tuple)) and len(open_circuit_voltage) > 0: - open_circuit_voltage = float(open_circuit_voltage[0]) - else: - open_circuit_voltage = float(open_circuit_voltage) - except Exception as e: - print(f"读取开路电压失败: {e}") - open_circuit_voltage = 0.0 - - try: - pole_weight = device.data_pole_weight # 正极片称重数据(g) - # 确保返回的是数值类型 - if isinstance(pole_weight, (list, tuple)) and len(pole_weight) > 0: - pole_weight = float(pole_weight[0]) - else: - pole_weight = float(pole_weight) - except Exception as e: - print(f"读取正极片重量失败: {e}") - pole_weight = 0.0 - - try: - assembly_pressure = device.data_assembly_pressure # 电池压制力(N) - # 确保返回的是数值类型 - if isinstance(assembly_pressure, (list, tuple)) and len(assembly_pressure) > 0: - assembly_pressure = int(assembly_pressure[0]) - else: - assembly_pressure = int(assembly_pressure) - except Exception as e: - print(f"读取压制力失败: {e}") - assembly_pressure = 0 - - try: - battery_qr_code = device.data_coin_cell_code # 电池二维码序列号 - # 处理字符串类型数据 - if isinstance(battery_qr_code, str): - battery_qr_code = battery_qr_code.strip() - else: - battery_qr_code = str(battery_qr_code) - except Exception as e: - print(f"读取电池二维码失败: {e}") - battery_qr_code = "N/A" - - try: - electrolyte_qr_code = device.data_electrolyte_code # 电解液二维码序列号 - # 处理字符串类型数据 - if isinstance(electrolyte_qr_code, str): - electrolyte_qr_code = electrolyte_qr_code.strip() - else: - electrolyte_qr_code = str(electrolyte_qr_code) - except Exception as e: - print(f"读取电解液二维码失败: {e}") - electrolyte_qr_code = "N/A" - - # 获取电池数量 - try: - battery_count = device.data_assembly_coin_cell_num - # 确保返回的是数值类型 - if isinstance(battery_count, (list, tuple)) and len(battery_count) > 0: - battery_count = int(battery_count[0]) - else: - battery_count = int(battery_count) - except Exception as e: - print(f"读取电池数量失败: {e}") - battery_count = 0 - - return { - 'Timestamp': timestamp, - 'Battery_Count': battery_count, - 'Assembly_Time': assembly_time, - 'Open_Circuit_Voltage': open_circuit_voltage, - 'Pole_Weight': pole_weight, - 'Assembly_Pressure': assembly_pressure, - 'Battery_Code': battery_qr_code, - 'Electrolyte_Code': electrolyte_qr_code - } - except Exception as e: - print(f"收集数据时出错: {e}") - return None - -def export_to_csv(data, csv_file_path): - """将数据导出到CSV文件""" - try: - # 检查文件是否存在,如果不存在则创建并写入表头 - file_exists = os.path.exists(csv_file_path) - - # 确保目录存在 - csv_dir = os.path.dirname(csv_file_path) - if csv_dir: - os.makedirs(csv_dir, exist_ok=True) - - # 确保数值字段为正确的数值类型,避免前导单引号问题 - processed_data = data.copy() - - # 处理数值字段,确保它们是数值类型而不是字符串,增强错误处理 - numeric_fields = ['Battery_Count', 'Assembly_Time', 'Open_Circuit_Voltage', 'Pole_Weight', 'Assembly_Pressure'] - for field in numeric_fields: - if field in processed_data: - try: - value = processed_data[field] - # 处理可能的列表或元组类型 - if isinstance(value, (list, tuple)) and len(value) > 0: - value = value[0] - - if field == 'Battery_Count' or field == 'Assembly_Pressure': - processed_data[field] = int(float(value)) # 先转float再转int,处理字符串数字 - else: - processed_data[field] = float(value) - except (ValueError, TypeError, IndexError) as e: - print(f"字段 {field} 类型转换失败: {e}, 使用默认值") - processed_data[field] = 0 if field == 'Battery_Count' else 0.0 - - # 处理字符串字段 - for field in ['Battery_Code', 'Electrolyte_Code']: - if field in processed_data: - try: - value = processed_data[field] - if isinstance(value, (list, tuple)) and len(value) > 0: - value = value[0] - processed_data[field] = str(value).strip() - except Exception as e: - print(f"字段 {field} 处理失败: {e}, 使用默认值") - processed_data[field] = "N/A" - - with open(csv_file_path, 'a', newline='', encoding='utf-8') as csvfile: - fieldnames = ['Timestamp', 'Battery_Count', 'Assembly_Time', 'Open_Circuit_Voltage', - 'Pole_Weight', 'Assembly_Pressure', 'Battery_QR_Code', 'Electrolyte_QR_Code'] - writer = csv.DictWriter(csvfile, fieldnames=fieldnames, quoting=csv.QUOTE_MINIMAL) - - # 如果文件不存在,写入表头 - if not file_exists: - writer.writeheader() - print(f"创建新的CSV文件: {csv_file_path}") - - # 写入数据 - writer.writerow(processed_data) - print(f"数据已导出到: {csv_file_path}") - - return True - except Exception as e: - print(f"导出CSV时出错: {e}") - return False - -def view_csv_content(csv_file_path, lines=10): - """查看CSV文件内容""" - try: - if not os.path.exists(csv_file_path): - print("CSV文件不存在") - return - - with open(csv_file_path, 'r', encoding='utf-8') as csvfile: - content = csvfile.readlines() - - if not content: - print("CSV文件为空") - return - - print(f"CSV文件内容 (显示最后{min(lines, len(content))}行):") - print("-" * 80) - - # 显示表头 - if len(content) > 0: - print(content[0].strip()) - print("-" * 80) - - # 显示最后几行数据 - start_line = max(1, len(content) - lines + 1) - for i in range(start_line, len(content)): - print(content[i].strip()) - - print("-" * 80) - print(f"总共 {len(content)-1} 条数据记录") - - except Exception as e: - print(f"读取CSV文件时出错: {e}") - -def interactive_demo(): - """ - 交互式演示模式(优化版) - """ - clear_screen() - print_header() - - print("正在初始化设备连接...") - print("设备地址: 192.168.1.20:502") - print("正在尝试连接...") - - try: - device = Coin_Cell_Assembly(address="192.168.1.20", port="502") - print("✓ 设备连接成功") - - # 测试设备数据读取 - print("正在测试设备数据读取...") - try: - test_count = device.data_assembly_coin_cell_num - print(f"✓ 当前电池数量: {test_count}") - except Exception as e: - print(f"⚠ 数据读取测试失败: {e}") - print("设备连接正常,但数据读取可能存在问题") - - except Exception as e: - print(f"✗ 设备连接失败: {e}") - print("请检查以下项目:") - print("1. 设备是否已开机并正常运行") - print("2. 网络连接是否正常") - print("3. 设备IP地址是否为192.168.1.20") - print("4. Modbus服务是否在端口502上运行") - input("按回车键退出...") - return - - csv_file_path = "battery_data_export.csv" - print(f"CSV文件路径: {os.path.abspath(csv_file_path)}") - print() - print("功能说明:") - print("- 支持手动导出当前设备数据到CSV文件") - print("- 包含六个关键数据: 组装时间、开路电压、正极片重量、电池码、电解液码") - print("- 电池码和电解液码可能显示为N/A(当二维码读取失败时)") - print("- 支持查看CSV文件内容和导出状态") - print("- 兼容原有的电池组装完成状态自动导出功能") - print() - - print_commands() - - while True: - try: - command = input("\n请输入命令 > ").strip().lower() - - if command == "start": - print("启动电池组装完成状态导出...") - try: - success, message = device.start_battery_completion_export(csv_file_path) - if success: - print(f"✓ {message}") - print("系统正在监控电池组装完成状态...") - else: - print(f"✗ {message}") - except Exception as e: - print(f"启动导出时出错: {e}") - - elif command == "stop": - print("停止导出...") - try: - success, message = device.stop_csv_export() - if success: - print(f"✓ {message}") - else: - print(f"✗ {message}") - except Exception as e: - print(f"停止导出时出错: {e}") - - elif command == "force": - print("强制继续CSV导出...") - try: - success, message = device.force_continue_csv_export() - if success: - print(f"✓ {message}") - print("注意: CSV导出将继续监控数据变化,即使设备处于停止状态") - else: - print(f"✗ {message}") - except AttributeError: - print("✗ 当前版本不支持强制继续功能") - except Exception as e: - print(f"✗ 强制继续失败: {e}") - - elif command == "detail": - print("=== 详细设备状态 ===") - print_status_info(device, csv_file_path) - - elif command == "status": - print_status_info(device, csv_file_path) - - elif command == "data": - print("读取当前设备数据...") - try: - data = collect_device_data(device) - if data: - print("\n=== 当前设备数据 ===") - print(f"时间戳: {data['Timestamp']}") - print(f"电池数量: {data['Battery_Count']}") - print(f"单颗电池组装时间: {data['Assembly_Time']:.2f} 秒") - print(f"开路电压值: {data['Open_Circuit_Voltage']:.4f} V") - print(f"正极片称重数据: {data['Pole_Weight']:.4f} g") - print(f"电池压制力: {data['Assembly_Pressure']} N") - print(f"电池二维码序列号: {data['Battery_Code']}") - print(f"电解液二维码序列号: {data['Electrolyte_Code']}") - print("===================") - else: - print("无法获取设备数据") - except Exception as e: - print(f"读取数据时出错: {e}") - - elif command == "count": - print("读取当前电池数量...") - try: - count = device.data_assembly_coin_cell_num - print(f"当前已完成电池数量: {count}") - except Exception as e: - print(f"读取电池数量时出错: {e}") - - elif command == "export": - print("正在收集设备数据并导出到CSV...") - data = collect_device_data(device) - if data: - print(f"收集到数据: 电池数量={data.get('Battery_Count', 'N/A')}, 组装时间={data.get('Assembly_Time', 'N/A')}s") - if export_to_csv(data, csv_file_path): - print("✓ 数据已成功导出到CSV文件") - print(f"导出数据: 时间={data['Timestamp']}, 电池数量={data['Battery_Count']}, 组装时间={data['Assembly_Time']}秒, " - f"电压={data['Open_Circuit_Voltage']}V, 重量={data['Pole_Weight']}g, 压制力={data['Assembly_Pressure']}N") - print(f"电池码={data['Battery_Code']}, 电解液码={data['Electrolyte_Code']}") - else: - print("✗ 导出失败") - else: - print("✗ 数据收集失败,无法导出!请检查设备连接状态。") - # 尝试重新连接设备 - try: - if hasattr(device, 'connect'): - device.connect() - print("尝试重新连接设备...") - except Exception as e: - print(f"重新连接失败: {e}") - - elif command == "setpath": - print("设置自定义CSV文件路径") - print(f"当前CSV文件路径: {csv_file_path}") - new_path = input("请输入新的CSV文件路径(包含文件名,如: D:/data/my_battery_data.csv): ").strip() - if new_path: - try: - # 确保目录存在 - new_dir = os.path.dirname(new_path) - if new_dir and not os.path.exists(new_dir): - os.makedirs(new_dir, exist_ok=True) - print(f"✓ 已创建目录: {new_dir}") - - csv_file_path = new_path - print(f"✓ CSV文件路径已更新为: {os.path.abspath(csv_file_path)}") - - # 检查文件是否存在 - if os.path.exists(csv_file_path): - file_size = os.path.getsize(csv_file_path) - print(f"文件已存在,大小: {file_size} 字节") - else: - print("文件不存在,将在首次导出时创建") - except Exception as e: - print(f"✗ 设置路径失败: {e}") - else: - print("路径不能为空") - - elif command == "view": - print("查看CSV文件内容...") - view_csv_content(csv_file_path) - - elif command == "clear": - clear_screen() - print_header() - print_commands() - - elif command == "help": - print_commands() - - elif command == "quit" or command == "exit": - print("正在退出...") - # 停止导出 - try: - device.stop_csv_export() - print("✓ 导出已停止") - except: - pass - print("程序已退出") - break - - elif command == "": - # 空命令,不做任何操作 - continue - - else: - print(f"未知命令: {command}") - print("输入 'help' 查看可用命令") - - except KeyboardInterrupt: - print("\n\n检测到 Ctrl+C,正在退出...") - try: - device.stop_csv_export() - print("✓ 导出已停止") - except: - pass - print("程序已退出") - break - except Exception as e: - print(f"执行命令时出错: {e}") - -if __name__ == '__main__': - interactive_demo() \ No newline at end of file diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index 09690f1..15d1bd5 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -178,6 +178,38 @@ bioyond_cell: title: create_orders参数 type: object type: UniLabJsonCommand + auto-create_orders_v2: + feedback: {} + goal: {} + goal_default: + xlsx_path: null + handles: + output: + - data_key: total_orders + data_source: executor + data_type: integer + handler_key: bottle_count + io_type: sink + label: 配液瓶数 + placeholder_keys: {} + result: {} + schema: + description: 从Excel解析并创建实验(V2版本) + properties: + feedback: {} + goal: + properties: + xlsx_path: + type: string + required: + - xlsx_path + type: object + result: {} + required: + - goal + title: create_orders_v2参数 + type: object + type: UniLabJsonCommand auto-create_sample: feedback: {} goal: {} @@ -594,6 +626,47 @@ bioyond_cell: title: transfer_1_to_2参数 type: object type: UniLabJsonCommand + auto-transfer_3_to_2: + feedback: {} + goal: {} + goal_default: + source_wh_id: 3a19debc-84b4-0359-e2d4-b3beea49348b + source_x: 1 + source_y: 1 + source_z: 1 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: 3-2 物料转运,从3号位置转运到2号位置 + properties: + feedback: {} + goal: + properties: + source_wh_id: + default: 3a19debc-84b4-0359-e2d4-b3beea49348b + description: 来源仓库ID + type: string + source_x: + default: 1 + description: 来源位置X坐标 + type: integer + source_y: + default: 1 + description: 来源位置Y坐标 + type: integer + source_z: + default: 1 + description: 来源位置Z坐标 + type: integer + required: [] + type: object + result: {} + required: + - goal + title: transfer_3_to_2参数 + type: object + type: UniLabJsonCommand auto-transfer_3_to_2_to_1: feedback: {} goal: {} diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml index e2cb91a..426dbca 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -194,7 +194,7 @@ coincellassemblyworkstation_device: type: string fujipian_juzhendianwei: default: 0 - description: 负极片矩阵点位 + description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) type: integer fujipian_panshu: default: 0 @@ -202,7 +202,7 @@ coincellassemblyworkstation_device: type: integer gemo_juzhendianwei: default: 0 - description: 隔膜矩阵点位 + description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) type: integer gemopanshu: default: 0 @@ -214,7 +214,7 @@ coincellassemblyworkstation_device: type: boolean qiangtou_juzhendianwei: default: 0 - description: 枪头盒矩阵点位 + description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) type: integer required: - elec_num @@ -493,6 +493,125 @@ coincellassemblyworkstation_device: title: func_read_data_and_output参数 type: object type: UniLabJsonCommand + auto-func_sendbottle_allpack_multi: + feedback: {} + goal: {} + goal_default: + assembly_pressure: 4200 + assembly_type: 7 + battery_clean_ignore: false + battery_pressure_mode: true + dual_drop_first_volume: 25 + dual_drop_mode: false + dual_drop_start_timing: false + dual_drop_suction_timing: false + elec_num: null + elec_use_num: null + elec_vol: 50 + file_path: /Users/sml/work + fujipian_juzhendianwei: 0 + fujipian_panshu: 0 + gemo_juzhendianwei: 0 + gemopanshu: 0 + lvbodian: true + qiangtou_juzhendianwei: 0 + handles: + input: + - data_key: elec_num + data_source: workflow + data_type: integer + handler_key: bottle_count + io_type: source + label: 配液瓶数 + required: true + placeholder_keys: {} + result: {} + schema: + description: 发送瓶数+简化组装函数(适用于第二批次及后续批次),合并了发送瓶数和简化组装流程 + properties: + feedback: {} + goal: + properties: + assembly_pressure: + default: 4200 + description: 电池压制力(N) + type: integer + assembly_type: + default: 7 + description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫) + type: integer + battery_clean_ignore: + default: false + description: 是否忽略电池清洁步骤 + type: boolean + battery_pressure_mode: + default: true + description: 是否启用压力模式 + type: boolean + dual_drop_first_volume: + default: 25 + description: 二次滴液第一次排液体积(μL) + type: integer + dual_drop_mode: + default: false + description: 电解液添加模式(false=单次滴液, true=二次滴液) + type: boolean + dual_drop_start_timing: + default: false + description: 二次滴液开始滴液时机(false=正极片前, true=正极片后) + type: boolean + dual_drop_suction_timing: + default: false + description: 二次滴液吸液时机(false=正常吸液, true=先吸液) + type: boolean + elec_num: + description: 电解液瓶数,如果在workflow中已通过handles连接上游(create_orders的bottle_count输出),则此参数会自动从上游获取,无需手动填写;如果单独使用此函数(没有上游连接),则必须手动填写电解液瓶数 + type: string + elec_use_num: + description: 每瓶电解液组装电池数 + type: string + elec_vol: + default: 50 + description: 电解液吸液量(μL) + type: integer + file_path: + default: /Users/sml/work + description: 实验记录保存路径 + type: string + fujipian_juzhendianwei: + default: 0 + description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + type: integer + fujipian_panshu: + default: 0 + description: 负极片盘数 + type: integer + gemo_juzhendianwei: + default: 0 + description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + type: integer + gemopanshu: + default: 0 + description: 隔膜盘数 + type: integer + lvbodian: + default: true + description: 是否使用铝箔垫片 + type: boolean + qiangtou_juzhendianwei: + default: 0 + description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + type: integer + required: + - elec_num + - elec_use_num + type: object + result: {} + required: + - goal + title: func_sendbottle_allpack_multi参数 + type: object + type: UniLabJsonCommand auto-func_stop_read_data: feedback: {} goal: {} diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 77ac533..e69de29 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -1,5794 +0,0 @@ -virtual_centrifuge: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - centrifuge: - feedback: - current_speed: current_speed - current_status: status - current_temp: current_temp - progress: progress - goal: - speed: speed - temp: temp - time: time - vessel: vessel - goal_default: - speed: 0.0 - temp: 0.0 - time: 0.0 - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_speed: - type: number - current_status: - type: string - current_temp: - type: number - progress: - type: number - required: - - progress - - current_speed - - current_temp - - current_status - title: Centrifuge_Feedback - type: object - goal: - properties: - speed: - type: number - temp: - type: number - time: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - speed - - time - - temp - title: Centrifuge_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Centrifuge_Result - type: object - required: - - goal - title: Centrifuge - type: object - type: Centrifuge - module: unilabos.devices.virtual.virtual_centrifuge:VirtualCentrifuge - status_types: - centrifuge_state: str - current_speed: float - current_temp: float - max_speed: float - max_temp: float - message: str - min_temp: float - progress: float - status: str - target_speed: float - target_temp: float - time_remaining: float - type: python - config_info: [] - description: Virtual Centrifuge for CentrifugeProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: transport - description: 需要离心的样品容器 - handler_key: centrifuge - io_type: target - label: centrifuge - side: NORTH - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - centrifuge_state: - type: string - current_speed: - type: number - current_temp: - type: number - max_speed: - type: number - max_temp: - type: number - message: - type: string - min_temp: - type: number - progress: - type: number - status: - type: string - target_speed: - type: number - target_temp: - type: number - time_remaining: - type: number - required: - - status - - centrifuge_state - - current_speed - - target_speed - - current_temp - - target_temp - - max_speed - - max_temp - - min_temp - - time_remaining - - progress - - message - type: object - version: 1.0.0 -virtual_column: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - run_column: - feedback: - current_status: current_status - processed_volume: processed_volume - progress: progress - goal: - column: column - from_vessel: from_vessel - to_vessel: to_vessel - goal_default: - column: '' - from_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - pct1: '' - pct2: '' - ratio: '' - rf: '' - solvent1: '' - solvent2: '' - to_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - message: current_status - return_info: current_status - success: success - schema: - description: '' - properties: - feedback: - properties: - progress: - type: number - status: - type: string - required: - - status - - progress - title: RunColumn_Feedback - type: object - goal: - properties: - column: - type: string - from_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: from_vessel - type: object - pct1: - type: string - pct2: - type: string - ratio: - type: string - rf: - type: string - solvent1: - type: string - solvent2: - type: string - to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: to_vessel - type: object - required: - - from_vessel - - to_vessel - - column - - rf - - pct1 - - pct2 - - solvent1 - - solvent2 - - ratio - title: RunColumn_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: RunColumn_Result - type: object - required: - - goal - title: RunColumn - type: object - type: RunColumn - module: unilabos.devices.virtual.virtual_column:VirtualColumn - status_types: - column_diameter: float - column_length: float - column_state: str - current_flow_rate: float - current_phase: str - current_status: str - final_volume: float - max_flow_rate: float - processed_volume: float - progress: float - status: str - type: python - config_info: [] - description: Virtual Column Chromatography Device for RunColumn Protocol Testing - handles: - - data_key: from_vessel - data_source: handle - data_type: transport - description: 样品输入口 - handler_key: columnin - io_type: target - label: columnin - side: WEST - - data_key: to_vessel - data_source: handle - data_type: transport - description: 产物输出口 - handler_key: columnout - io_type: source - label: columnout - side: EAST - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - column_diameter: - type: number - column_length: - type: number - column_state: - type: string - current_flow_rate: - type: number - current_phase: - type: string - current_status: - type: string - final_volume: - type: number - max_flow_rate: - type: number - processed_volume: - type: number - progress: - type: number - status: - type: string - required: - - status - - column_state - - current_flow_rate - - max_flow_rate - - column_length - - column_diameter - - processed_volume - - progress - - current_status - - current_phase - - final_volume - type: object - version: 1.0.0 -virtual_filter: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - filter: - feedback: - current_status: current_status - current_temp: current_temp - filtered_volume: filtered_volume - progress: progress - goal: - continue_heatchill: continue_heatchill - filtrate_vessel: filtrate_vessel - stir: stir - stir_speed: stir_speed - temp: temp - vessel: vessel - volume: volume - goal_default: - continue_heatchill: false - filtrate_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - stir: false - stir_speed: 0.0 - temp: 0.0 - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - volume: 0.0 - handles: {} - result: - message: message - return_info: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - current_temp: - type: number - filtered_volume: - type: number - progress: - type: number - required: - - progress - - current_temp - - filtered_volume - - current_status - title: Filter_Feedback - type: object - goal: - properties: - continue_heatchill: - type: boolean - filtrate_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: filtrate_vessel - type: object - stir: - type: boolean - stir_speed: - type: number - temp: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - volume: - type: number - required: - - vessel - - filtrate_vessel - - stir - - stir_speed - - temp - - continue_heatchill - - volume - title: Filter_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Filter_Result - type: object - required: - - goal - title: Filter - type: object - type: Filter - module: unilabos.devices.virtual.virtual_filter:VirtualFilter - status_types: - current_status: str - current_temp: float - filtered_volume: float - max_stir_speed: float - max_temp: float - max_volume: float - message: str - progress: float - status: str - type: python - config_info: [] - description: Virtual Filter for FilterProtocol Testing - handles: - - data_key: vessel_in - data_source: handle - data_type: transport - description: 需要过滤的样品容器 - handler_key: filterin - io_type: target - label: filter_in - side: NORTH - - data_key: filtrate_out - data_source: handle - data_type: transport - description: 滤液出口 - handler_key: filtrateout - io_type: source - label: filtrate_out - side: SOUTH - - data_key: retentate_out - data_source: handle - data_type: transport - description: 滤渣/固体出口 - handler_key: retentateout - io_type: source - label: retentate_out - side: EAST - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - current_status: - type: string - current_temp: - type: number - filtered_volume: - type: number - max_stir_speed: - type: number - max_temp: - type: number - max_volume: - type: number - message: - type: string - progress: - type: number - status: - type: string - required: - - status - - progress - - current_temp - - current_status - - filtered_volume - - message - - max_temp - - max_stir_speed - - max_volume - type: object - version: 1.0.0 -virtual_gas_source: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-is_open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_open的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_open参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - module: unilabos.devices.virtual.virtual_gas_source:VirtualGasSource - status_types: - status: str - type: python - config_info: [] - description: Virtual gas source - handles: - - data_key: fluid_out - data_source: executor - data_type: fluid - description: 气源出气口 - handler_key: gassource - io_type: source - label: gassource - side: SOUTH - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - status: - type: string - required: - - status - type: object - version: 1.0.0 -virtual_heatchill: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - heat_chill: - feedback: - status: status - goal: - purpose: purpose - stir: stir - stir_speed: stir_speed - temp: temp - time: time - vessel: vessel - goal_default: - pressure: '' - purpose: '' - reflux_solvent: '' - stir: false - stir_speed: 0.0 - temp: 0.0 - temp_spec: '' - time: '' - time_spec: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChill_Feedback - type: object - goal: - properties: - pressure: - type: string - purpose: - type: string - reflux_solvent: - type: string - stir: - type: boolean - stir_speed: - type: number - temp: - type: number - temp_spec: - type: string - time: - type: string - time_spec: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose - title: HeatChill_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: HeatChill_Result - type: object - required: - - goal - title: HeatChill - type: object - type: HeatChill - heat_chill_start: - feedback: - status: status - goal: - purpose: purpose - temp: temp - vessel: vessel - goal_default: - purpose: '' - temp: 0.0 - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChillStart_Feedback - type: object - goal: - properties: - purpose: - type: string - temp: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - temp - - purpose - title: HeatChillStart_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: HeatChillStart_Result - type: object - required: - - goal - title: HeatChillStart - type: object - type: HeatChillStart - heat_chill_stop: - feedback: - status: status - goal: - vessel: vessel - goal_default: - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChillStop_Feedback - type: object - goal: - properties: - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - title: HeatChillStop_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: HeatChillStop_Result - type: object - required: - - goal - title: HeatChillStop - type: object - type: HeatChillStop - module: unilabos.devices.virtual.virtual_heatchill:VirtualHeatChill - status_types: - is_stirring: bool - max_stir_speed: float - max_temp: float - min_temp: float - operation_mode: str - progress: float - remaining_time: float - status: str - stir_speed: float - type: python - config_info: [] - description: Virtual HeatChill for HeatChillProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: mechanical - description: 加热/冷却器的物理连接口 - handler_key: heatchill - io_type: source - label: heatchill - side: NORTH - icon: Heater.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - is_stirring: - type: boolean - max_stir_speed: - type: number - max_temp: - type: number - min_temp: - type: number - operation_mode: - type: string - progress: - type: number - remaining_time: - type: number - status: - type: string - stir_speed: - type: number - required: - - status - - operation_mode - - is_stirring - - stir_speed - - remaining_time - - progress - - max_temp - - min_temp - - max_stir_speed - type: object - version: 1.0.0 -virtual_multiway_valve: - category: - - virtual_device - class: - action_value_mappings: - auto-close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: close的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: close参数 - type: object - type: UniLabJsonCommand - auto-is_at_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: is_at_port参数 - type: object - type: UniLabJsonCommand - auto-is_at_position: - feedback: {} - goal: {} - goal_default: - position: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_position的参数schema - properties: - feedback: {} - goal: - properties: - position: - type: integer - required: - - position - type: object - result: {} - required: - - goal - title: is_at_position参数 - type: object - type: UniLabJsonCommand - auto-is_at_pump_position: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_pump_position的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_at_pump_position参数 - type: object - type: UniLabJsonCommand - auto-open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: open参数 - type: object - type: UniLabJsonCommand - auto-reset: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: reset的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset参数 - type: object - type: UniLabJsonCommand - auto-set_to_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_to_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: set_to_port参数 - type: object - type: UniLabJsonCommand - auto-set_to_pump_position: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_to_pump_position的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: set_to_pump_position参数 - type: object - type: UniLabJsonCommand - auto-switch_between_pump_and_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: switch_between_pump_and_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: switch_between_pump_and_port参数 - type: object - type: UniLabJsonCommand - set_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - set_valve_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - module: unilabos.devices.virtual.virtual_multiway_valve:VirtualMultiwayValve - status_types: - current_port: str - current_position: int - flow_path: str - status: str - target_position: int - valve_position: int - valve_state: str - type: python - config_info: [] - description: Virtual 8-Way Valve for flow direction control - handles: - - data_key: fluid_in - data_source: handle - data_type: fluid - description: 八通阀门进液口 - handler_key: transferpump - io_type: target - label: transferpump - side: NORTH - - data_key: fluid_port_1 - data_source: executor - data_type: fluid - description: 八通阀门端口1 - handler_key: '1' - io_type: source - label: '1' - side: NORTH - - data_key: fluid_port_2 - data_source: executor - data_type: fluid - description: 八通阀门端口2 - handler_key: '2' - io_type: source - label: '2' - side: EAST - - data_key: fluid_port_3 - data_source: executor - data_type: fluid - description: 八通阀门端口3 - handler_key: '3' - io_type: source - label: '3' - side: EAST - - data_key: fluid_port_4 - data_source: executor - data_type: fluid - description: 八通阀门端口4 - handler_key: '4' - io_type: source - label: '4' - side: SOUTH - - data_key: fluid_port_5 - data_source: executor - data_type: fluid - description: 八通阀门端口5 - handler_key: '5' - io_type: source - label: '5' - side: SOUTH - - data_key: fluid_port_6 - data_source: executor - data_type: fluid - description: 八通阀门端口6 - handler_key: '6' - io_type: source - label: '6' - side: WEST - - data_key: fluid_port_7 - data_source: executor - data_type: fluid - description: 八通阀门端口7 - handler_key: '7' - io_type: source - label: '7' - side: WEST - - data_key: fluid_port_8 - data_source: executor - data_type: fluid - description: 八通阀门端口8-特殊输入 - handler_key: '8' - io_type: target - label: '8' - side: WEST - - data_key: fluid_port_8 - data_source: executor - data_type: fluid - description: 八通阀门端口8 - handler_key: '8' - io_type: source - label: '8' - side: NORTH - icon: EightPipeline.webp - init_param_schema: - config: - properties: - port: - default: VIRTUAL - type: string - positions: - default: 8 - type: integer - required: [] - type: object - data: - properties: - current_port: - type: string - current_position: - type: integer - flow_path: - type: string - status: - type: string - target_position: - type: integer - valve_position: - type: integer - valve_state: - type: string - required: - - status - - valve_state - - current_position - - target_position - - current_port - - valve_position - - flow_path - type: object - version: 1.0.0 -virtual_rotavap: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - evaporate: - feedback: - current_device: current_device - status: status - goal: - pressure: pressure - stir_speed: stir_speed - temp: temp - time: time - vessel: vessel - goal_default: - pressure: 0.0 - solvent: '' - stir_speed: 0.0 - temp: 0.0 - time: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_device: - type: string - status: - type: string - time_remaining: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: time_remaining - type: object - time_spent: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: time_spent - type: object - required: - - status - - current_device - - time_spent - - time_remaining - title: Evaporate_Feedback - type: object - goal: - properties: - pressure: - type: number - solvent: - type: string - stir_speed: - type: number - temp: - type: number - time: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - pressure - - temp - - time - - stir_speed - - solvent - title: Evaporate_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: Evaporate_Result - type: object - required: - - goal - title: Evaporate - type: object - type: Evaporate - module: unilabos.devices.virtual.virtual_rotavap:VirtualRotavap - status_types: - current_temp: float - evaporated_volume: float - max_rotation_speed: float - max_temp: float - message: str - progress: float - remaining_time: float - rotation_speed: float - rotavap_state: str - status: str - vacuum_pressure: float - type: python - config_info: [] - description: Virtual Rotary Evaporator for EvaporateProtocol Testing - handles: - - data_key: vessel_in - data_source: handle - data_type: fluid - description: 样品连接口 - handler_key: samplein - io_type: target - label: sample_in - side: NORTH - - data_key: product_out - data_source: handle - data_type: fluid - description: 浓缩产物出口 - handler_key: productout - io_type: source - label: product_out - side: SOUTH - - data_key: solvent_out - data_source: handle - data_type: fluid - description: 冷凝溶剂出口 - handler_key: solventout - io_type: source - label: solvent_out - side: EAST - icon: Rotaryevaporator.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - current_temp: - type: number - evaporated_volume: - type: number - max_rotation_speed: - type: number - max_temp: - type: number - message: - type: string - progress: - type: number - remaining_time: - type: number - rotation_speed: - type: number - rotavap_state: - type: string - status: - type: string - vacuum_pressure: - type: number - required: - - status - - rotavap_state - - current_temp - - rotation_speed - - vacuum_pressure - - evaporated_volume - - progress - - message - - max_temp - - max_rotation_speed - - remaining_time - type: object - version: 1.0.0 -virtual_separator: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - separate: - feedback: - current_status: status - progress: progress - goal: - from_vessel: from_vessel - product_phase: product_phase - purpose: purpose - repeats: repeats - separation_vessel: separation_vessel - settling_time: settling_time - solvent: solvent - solvent_volume: solvent_volume - stir_speed: stir_speed - stir_time: stir_time - through: through - to_vessel: to_vessel - waste_phase_to_vessel: waste_phase_to_vessel - goal_default: - from_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - product_phase: '' - product_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - purpose: '' - repeats: 0 - separation_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - settling_time: 0.0 - solvent: '' - solvent_volume: '' - stir_speed: 0.0 - stir_time: 0.0 - through: '' - to_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - volume: '' - waste_phase_to_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - waste_vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - progress: - type: number - status: - type: string - required: - - status - - progress - title: Separate_Feedback - type: object - goal: - properties: - from_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: from_vessel - type: object - product_phase: - type: string - product_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: product_vessel - type: object - purpose: - type: string - repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer - separation_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: separation_vessel - type: object - settling_time: - type: number - solvent: - type: string - solvent_volume: - type: string - stir_speed: - type: number - stir_time: - type: number - through: - type: string - to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: to_vessel - type: object - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - volume: - type: string - waste_phase_to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: waste_phase_to_vessel - type: object - waste_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: waste_vessel - type: object - required: - - vessel - - purpose - - product_phase - - from_vessel - - separation_vessel - - to_vessel - - waste_phase_to_vessel - - product_vessel - - waste_vessel - - solvent - - solvent_volume - - volume - - through - - repeats - - stir_time - - stir_speed - - settling_time - title: Separate_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Separate_Result - type: object - required: - - goal - title: Separate - type: object - type: Separate - module: unilabos.devices.virtual.virtual_separator:VirtualSeparator - status_types: - has_phases: bool - message: str - phase_separation: bool - progress: float - separator_state: str - settling_time: float - status: str - stir_speed: float - volume: float - type: python - config_info: [] - description: Virtual Separator for SeparateProtocol Testing - handles: - - data_key: from_vessel - data_source: handle - data_type: fluid - description: 需要分离的混合液体输入口 - handler_key: separatorin - io_type: target - label: separator_in - side: NORTH - - data_key: bottom_outlet - data_source: executor - data_type: fluid - description: 下相(重相)液体输出口 - handler_key: bottomphaseout - io_type: source - label: bottom_phase_out - side: SOUTH - - data_key: mechanical_port - data_source: handle - data_type: mechanical - description: 用于连接搅拌器等机械设备的接口 - handler_key: bind - io_type: target - label: bind - side: WEST - icon: Separator.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - has_phases: - type: boolean - message: - type: string - phase_separation: - type: boolean - progress: - type: number - separator_state: - type: string - settling_time: - type: number - status: - type: string - stir_speed: - type: number - volume: - type: number - required: - - status - - separator_state - - volume - - has_phases - - phase_separation - - stir_speed - - settling_time - - progress - - message - type: object - version: 1.0.0 -virtual_solenoid_valve: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - auto-reset: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: reset的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset参数 - type: object - type: UniLabJsonCommandAsync - auto-toggle: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: toggle的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: toggle参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: - command: CLOSED - goal_default: {} - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - set_valve_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - message: message - success: success - valve_position: valve_position - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve - status_types: - is_open: bool - status: str - valve_position: str - valve_state: str - type: python - config_info: [] - description: Virtual Solenoid Valve for simple on/off flow control - handles: - - data_key: fluid_port_in - data_source: handle - data_type: fluid - description: 电磁阀的进液口 - handler_key: in - io_type: target - label: in - side: NORTH - - data_key: fluid_port_out - data_source: handle - data_type: fluid - description: 电磁阀的出液口 - handler_key: out - io_type: source - label: out - side: SOUTH - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - is_open: - type: boolean - status: - type: string - valve_position: - type: string - valve_state: - type: string - required: - - status - - valve_state - - is_open - - valve_position - type: object - version: 1.0.0 -virtual_solid_dispenser: - category: - - virtual_device - class: - action_value_mappings: - add_solid: - feedback: - current_status: status - progress: progress - goal: - equiv: equiv - event: event - mass: mass - mol: mol - purpose: purpose - rate_spec: rate_spec - ratio: ratio - reagent: reagent - vessel: vessel - goal_default: - amount: '' - equiv: '' - event: '' - mass: '' - mol: '' - purpose: '' - rate_spec: '' - ratio: '' - reagent: '' - stir: false - stir_speed: 0.0 - time: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - viscous: false - volume: '' - handles: {} - result: - message: message - return_info: return_info - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - required: - - progress - - current_status - title: Add_Feedback - type: object - goal: - properties: - amount: - type: string - equiv: - type: string - event: - type: string - mass: - type: string - mol: - type: string - purpose: - type: string - rate_spec: - type: string - ratio: - type: string - reagent: - type: string - stir: - type: boolean - stir_speed: - type: number - time: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - viscous: - type: boolean - volume: - type: string - required: - - vessel - - reagent - - volume - - mass - - amount - - time - - stir - - stir_speed - - viscous - - purpose - - event - - mol - - rate_spec - - equiv - - ratio - title: Add_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Add_Result - type: object - required: - - goal - title: Add - type: object - type: Add - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-find_solid_reagent_bottle: - feedback: {} - goal: {} - goal_default: - reagent_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - reagent_name: - type: string - required: - - reagent_name - type: object - result: {} - required: - - goal - title: find_solid_reagent_bottle参数 - type: object - type: UniLabJsonCommand - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-parse_mass_string: - feedback: {} - goal: {} - goal_default: - mass_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mass_str: - type: string - required: - - mass_str - type: object - result: {} - required: - - goal - title: parse_mass_string参数 - type: object - type: UniLabJsonCommand - auto-parse_mol_string: - feedback: {} - goal: {} - goal_default: - mol_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mol_str: - type: string - required: - - mol_str - type: object - result: {} - required: - - goal - title: parse_mol_string参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser - status_types: - current_reagent: str - dispensed_amount: float - status: str - total_operations: int - type: python - config_info: [] - description: Virtual Solid Dispenser for Add Protocol Testing - supports mass and - molar additions - handles: - - data_key: solid_out - data_source: executor - data_type: resource - description: 固体试剂输出口 - handler_key: SolidOut - io_type: source - label: SolidOut - side: SOUTH - - data_key: solid_in - data_source: handle - data_type: resource - description: 固体试剂输入口(连接试剂瓶) - handler_key: SolidIn - io_type: target - label: SolidIn - side: NORTH - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_reagent: - type: string - dispensed_amount: - type: number - status: - type: string - total_operations: - type: integer - required: - - status - - current_reagent - - dispensed_amount - - total_operations - type: object - version: 1.0.0 -virtual_stirrer: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - start_stir: - feedback: - status: status - goal: - purpose: purpose - stir_speed: stir_speed - vessel: vessel - goal_default: - purpose: '' - stir_speed: 0.0 - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - current_speed: - type: number - current_status: - type: string - progress: - type: number - required: - - progress - - current_speed - - current_status - title: StartStir_Feedback - type: object - goal: - properties: - purpose: - type: string - stir_speed: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - stir_speed - - purpose - title: StartStir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: StartStir_Result - type: object - required: - - goal - title: StartStir - type: object - type: StartStir - stir: - feedback: - status: status - goal: - settling_time: settling_time - stir_speed: stir_speed - stir_time: stir_time - goal_default: - event: '' - settling_time: '' - stir_speed: 0.0 - stir_time: 0.0 - time: '' - time_spec: '' - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: Stir_Feedback - type: object - goal: - properties: - event: - type: string - settling_time: - type: string - stir_speed: - type: number - stir_time: - type: number - time: - type: string - time_spec: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time - title: Stir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Stir_Result - type: object - required: - - goal - title: Stir - type: object - type: Stir - stop_stir: - feedback: - status: status - goal: - vessel: vessel - goal_default: - vessel: - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - required: - - progress - - current_status - title: StopStir_Feedback - type: object - goal: - properties: - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - title: StopStir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: StopStir_Result - type: object - required: - - goal - title: StopStir - type: object - type: StopStir - module: unilabos.devices.virtual.virtual_stirrer:VirtualStirrer - status_types: - current_speed: float - current_vessel: str - device_info: dict - is_stirring: bool - max_speed: float - min_speed: float - operation_mode: str - remaining_time: float - status: str - type: python - config_info: [] - description: Virtual Stirrer for StirProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: mechanical - description: 搅拌器的机械连接口 - handler_key: stirrer - io_type: source - label: stirrer - side: NORTH - icon: Stirrer.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_speed: - type: number - current_vessel: - type: string - device_info: - type: object - is_stirring: - type: boolean - max_speed: - type: number - min_speed: - type: number - operation_mode: - type: string - remaining_time: - type: number - status: - type: string - required: - - status - - operation_mode - - current_vessel - - current_speed - - is_stirring - - remaining_time - - max_speed - - min_speed - - device_info - type: object - version: 1.0.0 -virtual_transfer_pump: - category: - - virtual_device - class: - action_value_mappings: - auto-aspirate: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: aspirate的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: aspirate参数 - type: object - type: UniLabJsonCommandAsync - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-dispense: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: dispense的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: dispense参数 - type: object - type: UniLabJsonCommandAsync - auto-empty_syringe: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: empty_syringe的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: [] - type: object - result: {} - required: - - goal - title: empty_syringe参数 - type: object - type: UniLabJsonCommandAsync - auto-fill_syringe: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: fill_syringe的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: [] - type: object - result: {} - required: - - goal - title: fill_syringe参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_empty: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_empty的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_empty参数 - type: object - type: UniLabJsonCommand - auto-is_full: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_full的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_full参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - auto-pull_plunger: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: pull_plunger的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: pull_plunger参数 - type: object - type: UniLabJsonCommandAsync - auto-push_plunger: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: push_plunger的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: push_plunger参数 - type: object - type: UniLabJsonCommandAsync - auto-set_max_velocity: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_max_velocity的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: - - velocity - type: object - result: {} - required: - - goal - title: set_max_velocity参数 - type: object - type: UniLabJsonCommand - auto-stop_operation: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: stop_operation的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: stop_operation参数 - type: object - type: UniLabJsonCommandAsync - set_position: - feedback: - current_position: current_position - progress: progress - status: status - goal: - max_velocity: max_velocity - position: position - goal_default: - max_velocity: 0.0 - position: 0.0 - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_position: - type: number - progress: - type: number - status: - type: string - required: - - status - - current_position - - progress - title: SetPumpPosition_Feedback - type: object - goal: - properties: - max_velocity: - type: number - position: - type: number - required: - - position - - max_velocity - title: SetPumpPosition_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - - message - title: SetPumpPosition_Result - type: object - required: - - goal - title: SetPumpPosition - type: object - type: SetPumpPosition - transfer: - feedback: - current_status: current_status - progress: progress - transferred_volume: transferred_volume - goal: - amount: amount - from_vessel: from_vessel - rinsing_repeats: rinsing_repeats - rinsing_solvent: rinsing_solvent - rinsing_volume: rinsing_volume - solid: solid - time: time - to_vessel: to_vessel - viscous: viscous - volume: volume - goal_default: - amount: '' - from_vessel: '' - rinsing_repeats: 0 - rinsing_solvent: '' - rinsing_volume: 0.0 - solid: false - time: 0.0 - to_vessel: '' - viscous: false - volume: 0.0 - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - transferred_volume: - type: number - required: - - progress - - transferred_volume - - current_status - title: Transfer_Feedback - type: object - goal: - properties: - amount: - type: string - from_vessel: - type: string - rinsing_repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer - rinsing_solvent: - type: string - rinsing_volume: - type: number - solid: - type: boolean - time: - type: number - to_vessel: - type: string - viscous: - type: boolean - volume: - type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid - title: Transfer_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Transfer_Result - type: object - required: - - goal - title: Transfer - type: object - type: Transfer - module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump - status_types: - current_volume: float - max_velocity: float - position: float - remaining_capacity: float - status: str - transfer_rate: float - type: python - config_info: [] - description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style) - handles: - - data_key: fluid_port - data_source: handle - data_type: fluid - description: 注射器式转移泵的连接口 - handler_key: transferpump - io_type: source - label: transferpump - side: SOUTH - icon: Pump.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_volume: - type: number - max_velocity: - type: number - position: - type: number - remaining_capacity: - type: number - status: - type: string - transfer_rate: - type: number - required: - - status - - position - - current_volume - - max_velocity - - transfer_rate - - remaining_capacity - type: object - version: 1.0.0 -virtual_vacuum_pump: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-is_open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_open的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_open参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - module: unilabos.devices.virtual.virtual_vacuum_pump:VirtualVacuumPump - status_types: - status: str - type: python - config_info: [] - description: Virtual vacuum pump - handles: - - data_key: fluid_in - data_source: handle - data_type: fluid - description: 真空泵进气口 - handler_key: vacuumpump - io_type: source - label: vacuumpump - side: SOUTH - icon: Vacuum.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - status: - type: string - required: - - status - type: object - version: 1.0.0 diff --git a/unilabos/resources/bioyond/README_WAREHOUSE.md b/unilabos/resources/bioyond/README_WAREHOUSE.md new file mode 100644 index 0000000..6d1a62e --- /dev/null +++ b/unilabos/resources/bioyond/README_WAREHOUSE.md @@ -0,0 +1,548 @@ +# Bioyond 仓库系统开发指南 + +本文档详细说明 Bioyond 仓库(Warehouse)系统的架构、配置和使用方法,帮助开发者快速理解和维护仓库相关代码。 + +## 📚 目录 + +- [系统架构](#系统架构) +- [核心概念](#核心概念) +- [三层映射关系](#三层映射关系) +- [warehouse_factory 详解](#warehouse_factory-详解) +- [创建新仓库](#创建新仓库) +- [常见问题](#常见问题) +- [调试技巧](#调试技巧) + +--- + +## 系统架构 + +Bioyond 仓库系统采用**三层架构**,实现从前端显示到后端 API 的完整映射: + +``` +┌─────────────────────────────────────────────────────────┐ +│ 前端显示层 (YB_warehouses.py) │ +│ - warehouse_factory 自动生成库位网格 │ +│ - 生成库位名称:A01, B02, C03... │ +│ - 存储在 WareHouse.sites 字典中 │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Deck 布局层 (decks.py) │ +│ - 定义仓库在 Deck 上的物理位置 │ +│ - 组织多个仓库形成完整布局 │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ UUID 映射层 (config.py) │ +│ - 将库位名称映射到 Bioyond 系统 UUID │ +│ - 用于 API 调用时的物料入库操作 │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 核心概念 + +### 仓库(Warehouse) + +仓库是一个**三维网格**,用于存放物料。由以下参数定义: + +- **num_items_x**: 列数(X 轴) +- **num_items_y**: 行数(Y 轴) +- **num_items_z**: 层数(Z 轴) + +例如:`5行×3列×1层` = 5×3×1 = 15个库位 + +### 库位(Site) + +库位是仓库中的单个存储位置,由**字母行+数字列**命名: + +- **字母行**:A, B, C, D, E, F...(对应 Y 轴) +- **数字列**:01, 02, 03, 04...(对应 X 轴或 Z 轴) + +示例:`A01`, `B02`, `C03` + +### 布局模式(Layout) + +控制库位的排序和 Y 坐标计算: + +| 模式 | 说明 | 生成顺序 | Y 坐标计算 | 显示效果 | +|------|------|----------|-----------|---------| +| `col-major` | 列优先(默认) | A01, B01, C01, A02... | `dy + (num_y - row - 1) * item_dy` | A 可能在下 | +| `row-major` | 行优先 | A01, A02, A03, B01... | `dy + row * item_dy` | **A 在上** ✓ | + +**重要:** 使用 `row-major` 可以避免上下颠倒问题! + +--- + +## 三层映射关系 + +### 示例:手动传递窗右(A01-E03) + +#### 1️⃣ 前端显示层 - [`YB_warehouses.py`](YB_warehouses.py) + +```python +def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse: + """创建 5行×3列×1层 仓库""" + return warehouse_factory( + name=name, + num_items_x=3, # 3列 + num_items_y=5, # 5行 + num_items_z=1, # 1层 + row_offset=row_offset, + layout="row-major", + ) +``` + +**自动生成的库位:** A01, A02, A03, B01, B02, B03, ..., E01, E02, E03 + +#### 2️⃣ Deck 布局层 - [`decks.py`](decks.py) + +```python +self.warehouses = { + "手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), +} +self.warehouse_locations = { + "手动传递窗右": Coordinate(4160.0, 877.0, 0.0), +} +``` + +**作用:** +- 创建仓库实例 +- 设置在 Deck 上的物理坐标 + +#### 3️⃣ UUID 映射层 - [`config.py`](../../devices/workstation/bioyond_studio/config.py) + +```python +WAREHOUSE_MAPPING = { + "手动传递窗右": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", + "A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe", + # ... 其他库位 + } + } +} +``` + +**作用:** +- 用户拖拽物料到"手动传递窗右"的"A01"位置时 +- 系统查找 `WAREHOUSE_MAPPING["手动传递窗右"]["site_uuids"]["A01"]` +- 获取 UUID `"3a19deae-2c7a-36f5-5e41-02c5b66feaea"` +- 调用 Bioyond API 将物料入库到该 UUID 位置 + +--- + +## 实际配置案例 + +### 案例:手动传递窗左/右的完整配置 + +本案例展示如何为"手动传递窗右"和"手动传递窗左"建立完整的三层映射。 + +#### 背景需求 +- **手动传递窗右**: 需要 A01-E03(5行×3列=15个库位) +- **手动传递窗左**: 需要 F01-J03(5行×3列=15个库位) +- 这两个仓库共享同一个物理堆栈的 UUID("手动堆栈") + +#### 实施步骤 + +**1️⃣ 修复前端布局** - [`YB_warehouses.py`](YB_warehouses.py) + +```python +# 创建新的 5×3×1 仓库函数(之前是错误的 1×3×3) +def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse: + """创建5行×3列×1层仓库,支持行偏移生成不同字母行""" + return warehouse_factory( + name=name, + num_items_x=3, # 3列 + num_items_y=5, # 5行 ← 修正 + num_items_z=1, # 1层 ← 修正 + row_offset=row_offset, # ← 支持 F-J 行 + layout="row-major", # ← 避免上下颠倒 + ) +``` + +**2️⃣ 更新 Deck 配置** - [`decks.py`](decks.py) + +```python +from unilabos.resources.bioyond.YB_warehouses import ( + bioyond_warehouse_5x3x1, # 新增导入 +) + +class BIOYOND_YB_Deck(Deck): + def setup(self) -> None: + self.warehouses = { + # 修改前: bioyond_warehouse_1x3x3 (错误尺寸) + # 修改后: bioyond_warehouse_5x3x1 (正确尺寸) + "手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03 + "手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03 + } +``` + +**3️⃣ 添加 UUID 映射** - [`config.py`](../../devices/workstation/bioyond_studio/config.py) + +```python +WAREHOUSE_MAPPING = { + # 保持原有的"手动堆栈"配置不变(A01-J03共30个库位) + "手动堆栈": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", + # ... A02-E03 共15个 + "F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a", + # ... F02-J03 共15个 + } + }, + + # [新增] 手动传递窗右 - 复用"手动堆栈"的 A01-E03 UUID + "手动传递窗右": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", # ← 与手动堆栈A01相同 + "A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe", + "A03": "3a19deae-2c7a-5876-c454-6b7e224ca927", + "B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a", + "C01": "3a19deae-2c7a-32bc-768e-556647e292f3", + "C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4", + "C03": "3a19deae-2c7a-3056-6504-10dc73fbc276", + "D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440", + "D02": "3a19deae-2c7a-61be-601c-b6fb5610499a", + "D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560", + "E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363", + "E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910", + "E03": "3a19deae-2c7a-b163-2219-23df15200311", + } + }, + + # [新增] 手动传递窗左 - 复用"手动堆栈"的 F01-J03 UUID + "手动传递窗左": { + "uuid": "", + "site_uuids": { + "F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a", # ← 与手动堆栈F01相同 + "F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679", + "F03": "3a19deae-2c7a-f7c4-12bd-425799425698", + "G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955", + "G02": "3a19deae-2c7a-204e-95ed-1f1950f28343", + "G03": "3a19deae-2c7a-392b-62f1-4907c66343f8", + "H01": "3a19deae-2c7a-5602-e876-d27aca4e3201", + "H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702", + "H03": "3a19deae-2c7a-780b-8965-2e1345f7e834", + "I01": "3a19deae-2c7a-8849-e172-07de14ede928", + "I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0", + "I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4", + "J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6", + "J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205", + "J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9" + } + }, +} +``` + +#### 关键要点 + +1. **UUID 可以复用**: 三个仓库(手动堆栈、手动传递窗右、手动传递窗左)可以共享相同的物理库位 UUID +2. **库位名称必须匹配**: 前端生成的库位名称(如 F01)必须与 config.py 中的键名完全一致 +3. **row_offset 的妙用**: + - `row_offset=0` → 生成 A-E 行 + - `row_offset=5` → 生成 F-J 行(跳过前5个字母) + +#### 验证结果 + +配置完成后,拖拽测试: + +| 拖拽位置 | 前端库位 | 查找路径 | UUID | 结果 | +|---------|---------|---------|------|------| +| 手动传递窗右/A01 | A01 | `WAREHOUSE_MAPPING["手动传递窗右"]["site_uuids"]["A01"]` | `3a19...eaea` | ✅ 正确入库 | +| 手动传递窗左/F01 | F01 | `WAREHOUSE_MAPPING["手动传递窗左"]["site_uuids"]["F01"]` | `3a19...c4a` | ✅ 正确入库 | +| 手动堆栈/A01 | A01 | `WAREHOUSE_MAPPING["手动堆栈"]["site_uuids"]["A01"]` | `3a19...eaea` | ✅ 仍然正常 | + + +--- + +## warehouse_factory 详解 + +### 函数签名 + +```python +def warehouse_factory( + name: str, + num_items_x: int = 1, # 列数 + num_items_y: int = 4, # 行数 + num_items_z: int = 4, # 层数 + dx: float = 137.0, # X 起始偏移 + dy: float = 96.0, # Y 起始偏移 + dz: float = 120.0, # Z 起始偏移 + item_dx: float = 10.0, # X 间距 + item_dy: float = 10.0, # Y 间距 + item_dz: float = 10.0, # Z 间距 + col_offset: int = 0, # 列偏移(影响数字) + row_offset: int = 0, # 行偏移(影响字母) + layout: str = "col-major", # 布局模式 +) -> WareHouse: +``` + +### 参数说明 + +#### 尺寸参数 +- **num_items_x, y, z**: 定义仓库的网格尺寸 +- **注意**: 当 `num_items_z > 1` 时,Z 轴会被映射为数字列 + +#### 位置参数 +- **dx, dy, dz**: 第一个库位的起始坐标 +- **item_dx, dy, dz**: 库位之间的间距 + +#### 偏移参数 +- **col_offset**: 列起始偏移,用于生成 A05-D08 等命名 + ```python + col_offset=4 # 生成 A05, A06, A07, A08 + ``` + +- **row_offset**: 行起始偏移,用于生成 F01-J03 等命名 + ```python + row_offset=5 # 生成 F01, F02, F03(跳过 A-E) + ``` + +#### 布局参数 +- **layout**: + - `"col-major"`: 列优先(默认),可能导致上下颠倒 + - `"row-major"`: 行优先,**推荐使用**,A 显示在上 + +### 库位生成逻辑 + +```python +# row-major 模式(推荐) +keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}" + for j in range(num_y) + for i in range(num_x)] + +# 示例:num_y=2, num_x=3, row_offset=0, col_offset=0 +# 生成:A01, A02, A03, B01, B02, B03 +``` + +### Y 坐标计算 + +```python +if layout == "row-major": + # A 在上(Y 较小) + y = dy + row * item_dy +else: + # A 在下(Y 较大)- 不推荐 + y = dy + (num_items_y - row - 1) * item_dy +``` + +--- + +## 创建新仓库 + +### 步骤 1: 在 YB_warehouses.py 中创建函数 + +```python +def bioyond_warehouse_3x4x1(name: str) -> WareHouse: + """创建 3行×4列×1层 仓库 + + 布局: + A01 | A02 | A03 | A04 + B01 | B02 | B03 | B04 + C01 | C02 | C03 | C04 + """ + return warehouse_factory( + name=name, + num_items_x=4, # 4列 + num_items_y=3, # 3行 + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=120.0, + item_dz=120.0, + category="warehouse", + layout="row-major", # ⭐ 推荐使用 + ) +``` + +### 步骤 2: 在 decks.py 中使用 + +```python +# 1. 导入函数 +from unilabos.resources.bioyond.YB_warehouses import ( + bioyond_warehouse_3x4x1, # 新增 +) + +# 2. 在 setup() 中添加 +self.warehouses = { + "我的新仓库": bioyond_warehouse_3x4x1("我的新仓库"), +} +self.warehouse_locations = { + "我的新仓库": Coordinate(100.0, 200.0, 0.0), +} +``` + +### 步骤 3: 在 config.py 中配置 UUID(可选) + +```python +WAREHOUSE_MAPPING = { + "我的新仓库": { + "uuid": "", + "site_uuids": { + "A01": "从 Bioyond 系统获取的 UUID", + "A02": "从 Bioyond 系统获取的 UUID", + # ... 其他 11 个库位 + } + } +} +``` + +**注意:** 如果不需要拖拽入库功能,可跳过此步骤。 + +--- + +## 常见问题 + +### Q1: 为什么库位显示上下颠倒(C 在上,A 在下)? + +**原因:** 使用了默认的 `col-major` 布局。 + +**解决:** 在 `warehouse_factory` 中添加 `layout="row-major"` + +```python +return warehouse_factory( + ... + layout="row-major", # ← 添加这行 +) +``` + +### Q2: 我需要 1×3×3 还是 3×3×1? + +**判断方法:** +- **1×3×3**: 1列×3行×3**层**(垂直堆叠,有高度) +- **3×3×1**: 3行×3列×1**层**(平面网格) + +**推荐:** 大多数情况使用 `X×Y×1`(平面网格)更直观。 + +### Q3: 如何生成 F01-J03 而非 A01-E03? + +**方法:** 使用 `row_offset` 参数 + +```python +bioyond_warehouse_5x3x1("仓库名", row_offset=5) +# row_offset=5 跳过 A-E,从 F 开始 +``` + +### Q4: 拖拽物料后找不到 UUID 怎么办? + +**检查清单:** +1. `config.py` 中是否有该仓库的配置? +2. 仓库名称是否完全匹配? +3. 库位名称(如 A01)是否在 `site_uuids` 中? + +**示例错误:** +```python +# decks.py +"手动传递窗右": bioyond_warehouse_5x3x1(...) + +# config.py - ❌ 名称不匹配 +"手动传递窗": { ... } # 缺少"右"字 +``` + +### Q5: 库位重叠怎么办? + +**原因:** 间距(`item_dx/dy/dz`)太小。 + +**解决:** 增大间距参数 + +```python +item_dx=150.0, # 增大 X 间距 +item_dy=130.0, # 增大 Y 间距 +``` + +--- + +## 调试技巧 + +### 1. 查看生成的库位 + +```python +warehouse = bioyond_warehouse_5x3x1("测试仓库") +print(list(warehouse.sites.keys())) +# 输出:['A01', 'A02', 'A03', 'B01', 'B02', ...] +``` + +### 2. 检查库位坐标 + +```python +for name, site in warehouse.sites.items(): + print(f"{name}: {site.location}") +# 输出: +# A01: Coordinate(x=10.0, y=10.0, z=120.0) +# A02: Coordinate(x=147.0, y=10.0, z=120.0) +# ... +``` + +### 3. 验证 UUID 映射 + +```python +from unilabos.devices.workstation.bioyond_studio.config import WAREHOUSE_MAPPING + +warehouse_name = "手动传递窗右" +location_code = "A01" + +if warehouse_name in WAREHOUSE_MAPPING: + uuid = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"].get(location_code) + print(f"{warehouse_name}/{location_code} → {uuid}") +else: + print(f"❌ 未找到仓库: {warehouse_name}") +``` + +--- + +## 文件关系图 + +``` +unilabos/ +├── resources/ +│ ├── warehouse.py # warehouse_factory 核心实现 +│ └── bioyond/ +│ ├── YB_warehouses.py # ⭐ 仓库函数定义 +│ ├── decks.py # ⭐ Deck 布局配置 +│ └── README_WAREHOUSE.md # 📖 本文档 +└── devices/ + └── workstation/ + └── bioyond_studio/ + ├── config.py # ⭐ UUID 映射配置 + └── bioyond_cell/ + └── bioyond_cell_workstation.py # 业务逻辑 +``` + +--- + +## 版本历史 + +- **v1.1** (2026-01-08): 补充实际配置案例 + - 添加"手动传递窗右"和"手动传递窗左"的完整配置示例 + - 展示 UUID 复用的实际应用 + - 说明三个仓库共享物理堆栈的配置方法 + +- **v1.0** (2026-01-07): 初始版本 + - 新增 `row_offset` 参数支持 + - 创建 `bioyond_warehouse_5x3x1` 和 `bioyond_warehouse_2x2x1` + - 修复多个仓库的上下颠倒问题 + +--- + +## 相关资源 + +- [warehouse.py](../warehouse.py) - 核心工厂函数实现 +- [YB_warehouses.py](YB_warehouses.py) - 所有仓库定义 +- [decks.py](decks.py) - Deck 布局配置 +- [config.py](../../devices/workstation/bioyond_studio/config.py) - UUID 映射 + +--- + +**维护者:** Uni-Lab-OS 开发团队 +**最后更新:** 2026-01-07 diff --git a/unilabos/resources/bioyond/YB_warehouses.py b/unilabos/resources/bioyond/YB_warehouses.py index ae9e473..d39c0e8 100644 --- a/unilabos/resources/bioyond/YB_warehouses.py +++ b/unilabos/resources/bioyond/YB_warehouses.py @@ -166,7 +166,14 @@ def bioyond_warehouse_1x4x2(name: str) -> WareHouse: ) def bioyond_warehouse_1x2x2(name: str) -> WareHouse: - """创建BioYond 1x2x2仓库""" + """创建BioYond 1x2x2仓库(1列×2行×2层)- 旧版本,已弃用 + + 布局(2层): + 层1: A01 + B01 + 层2: A02 + B02 + """ return warehouse_factory( name=name, num_items_x=1, @@ -179,8 +186,32 @@ def bioyond_warehouse_1x2x2(name: str) -> WareHouse: item_dy=96.0, item_dz=120.0, category="warehouse", + layout="row-major", # 使用行优先避免上下颠倒 ) +def bioyond_warehouse_2x2x1(name: str) -> WareHouse: + """创建BioYond 2x2x1仓库(2行×2列×1层) + + 布局: + A01 | A02 + B01 | B02 + """ + return warehouse_factory( + name=name, + num_items_x=2, # 2列 + num_items_y=2, # 2行 + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + layout="row-major", # 使用行优先避免上下颠倒 + ) + + def bioyond_warehouse_10x1x1(name: str) -> WareHouse: """创建BioYond 10x1x1仓库""" return warehouse_factory( @@ -208,11 +239,61 @@ def bioyond_warehouse_1x3x3(name: str) -> WareHouse: dy=10.0, dz=10.0, item_dx=137.0, - item_dy=96.0, + item_dy=120.0, # 增大Y方向间距以避免重叠 item_dz=120.0, category="warehouse", ) +def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse: + """创建BioYond 5x3x1仓库(5行×3列×1层) + + 标准布局(row_offset=0): + A01 | A02 | A03 + B01 | B02 | B03 + C01 | C02 | C03 + D01 | D02 | D03 + E01 | E02 | E03 + + 带偏移布局(row_offset=5): + F01 | F02 | F03 + G01 | G02 | G03 + H01 | H02 | H03 + I01 | I02 | I03 + J01 | J02 | J03 + """ + return warehouse_factory( + name=name, + num_items_x=3, # 3列 + num_items_y=5, # 5行 + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=120.0, + item_dz=120.0, + category="warehouse", + col_offset=0, + row_offset=row_offset, # 支持行偏移 + layout="row-major", # 使用行优先避免颠倒 + ) + + +def bioyond_warehouse_3x3x1(name: str) -> WareHouse: + """创建BioYond 3x3x1仓库""" + return warehouse_factory( + name=name, + num_items_x=3, + num_items_y=3, + num_items_z=1, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) def bioyond_warehouse_2x1x3(name: str) -> WareHouse: """创建BioYond 2x1x3仓库""" return warehouse_factory( diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index 67e6d79..f56b2ba 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -8,7 +8,9 @@ from unilabos.resources.bioyond.YB_warehouses import ( bioyond_warehouse_reagent_stack, # 新增:试剂堆栈 (A1-B4) bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, + bioyond_warehouse_2x2x1, # 新增:321和43窗口 (2行×2列) bioyond_warehouse_1x3x3, + bioyond_warehouse_5x3x1, # 新增:手动传递窗仓库 (5行×3列) bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, @@ -115,10 +117,10 @@ class BIOYOND_YB_Deck(Deck): def setup(self) -> None: # 添加仓库 self.warehouses = { - "321窗口": bioyond_warehouse_1x2x2("321窗口"), - "43窗口": bioyond_warehouse_1x2x2("43窗口"), - "手动传递窗左": bioyond_warehouse_1x3x3("手动传递窗左"), - "手动传递窗右": bioyond_warehouse_1x3x3("手动传递窗右"), + "321窗口": bioyond_warehouse_2x2x1("321窗口"), # 2行×2列 + "43窗口": bioyond_warehouse_2x2x1("43窗口"), # 2行×2列 + "手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03 + "手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03 "加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"), "加样头堆栈右": bioyond_warehouse_10x1x1("加样头堆栈右"), diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py index 4dcda6d..3dbd6ad 100644 --- a/unilabos/resources/warehouse.py +++ b/unilabos/resources/warehouse.py @@ -27,6 +27,7 @@ def warehouse_factory( category: str = "warehouse", model: Optional[str] = None, col_offset: int = 0, # 列起始偏移量,用于生成A05-D08等命名 + row_offset: int = 0, # 行起始偏移量,用于生成F01-J03等命名 layout: str = "col-major", # 新增:排序方式,"col-major"=列优先,"row-major"=行优先 ): # 创建位置坐标 @@ -65,10 +66,10 @@ def warehouse_factory( if layout == "row-major": # 行优先顺序: A01,A02,A03,A04, B01,B02,B03,B04 # locations[0] 对应 row=0, y最大(前端顶部)→ 应该是 A01 - keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for j in range(len_y) for i in range(len_x)] + keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}" for j in range(len_y) for i in range(len_x)] else: # 列优先顺序: A01,B01,C01,D01, A02,B02,C02,D02 - keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)] + keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)] sites = {i: site for i, site in zip(keys, _sites.values())}