From 2eb9986edb662c73d7f91369d24df48a0b456f75 Mon Sep 17 00:00:00 2001 From: lixinyu1011 <674842481@qq.com> Date: Fri, 31 Oct 2025 13:54:58 +0800 Subject: [PATCH 1/4] 123 --- .../workstation/coin_cell_assembly.zip | Bin 0 -> 19794 bytes .../{coin_cell_assembly => }/__init__.py | 0 .../button_battery_station.py | 0 .../coin_cell_assembly.py | 14 +- .../coin_cell_assembly_a.csv | 0 .../new_cellconfig3c.json | 0 .../coin_cell_assembly/workstation_base.py | 489 ------------------ 7 files changed, 7 insertions(+), 496 deletions(-) create mode 100644 unilabos/devices/workstation/coin_cell_assembly.zip rename unilabos/devices/workstation/coin_cell_assembly/{coin_cell_assembly => }/__init__.py (100%) rename unilabos/devices/workstation/coin_cell_assembly/{coin_cell_assembly => }/button_battery_station.py (100%) rename unilabos/devices/workstation/coin_cell_assembly/{coin_cell_assembly => }/coin_cell_assembly.py (99%) rename unilabos/devices/workstation/coin_cell_assembly/{coin_cell_assembly => }/coin_cell_assembly_a.csv (100%) rename unilabos/devices/workstation/coin_cell_assembly/{coin_cell_assembly => }/new_cellconfig3c.json (100%) delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/workstation_base.py diff --git a/unilabos/devices/workstation/coin_cell_assembly.zip b/unilabos/devices/workstation/coin_cell_assembly.zip new file mode 100644 index 0000000000000000000000000000000000000000..b95b7f4a3f731c9d33a00a977343af3c5962ded5 GIT binary patch literal 19794 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 None: - """从给定的状态加载工作台信息。""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - data = super().serialize_state() - data.update( - self._unilabos_state - ) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - -def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法 - """ - 用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate,这里需要到注册表注册,例如unilabos/registry/resources/organic/workstation.yaml - 可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错 - :param name: 资源名称 - :return: Resource对象 - """ - plate = WorkStationContainer( - name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict() - ) - tip_rack = WorkStationContainer( - "tip_rack_inside_plate", - size_x=50, - size_y=50, - size_z=10, - category="tip_rack", - ordering=collections.OrderedDict(), - ) - plate.assign_child_resource(tip_rack, Coordinate.zero()) - return plate - - -class ResourceSynchronizer(ABC): - """资源同步器基类 - - 负责与外部物料系统的同步,并对 self.deck 做修改 - """ - - def __init__(self, workstation: "WorkstationBase"): - self.workstation = workstation - - @abstractmethod - async def sync_from_external(self) -> bool: - """从外部系统同步物料到本地deck""" - pass - - @abstractmethod - async def sync_to_external(self, plr_resource: PLRResource) -> bool: - """将本地物料同步到外部系统""" - pass - - @abstractmethod - async def handle_external_change(self, change_info: Dict[str, Any]) -> bool: - """处理外部系统的变更通知""" - pass - - -class WorkstationBase(ABC): - """工作站基类 - 简化版 - - 核心功能: - 1. 基于 PLR Deck 的物料系统,支持格式转换 - 2. 可选的资源同步器支持外部物料系统 - 3. 简化的工作流管理 - """ - - _ros_node: ROS2WorkstationNode - - @property - def _children(self) -> Dict[str, Any]: # 不要删除这个下划线,不然会自动导入注册表,后面改成装饰器识别 - return self._ros_node.children - - async def update_resource_example(self): - return await self._ros_node.update_resource([get_workstation_plate_resource("test")]) - - def __init__( - self, - station_resource: PLRResource, - *args, - **kwargs, # 必须有kwargs - ): - # 基本配置 - print(station_resource) - self.deck_config = station_resource - - # PLR 物料系统 - self.deck: Optional[Deck] = None - self.plr_resources: Dict[str, PLRResource] = {} - - # 资源同步器(可选) - # self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化,只有workstation用 - - # 硬件接口 - self.hardware_interface: Union[Any, str] = None - - # 工作流状态 - self.current_workflow_status = WorkflowStatus.IDLE - self.current_workflow_info = None - self.workflow_start_time = None - self.workflow_parameters = {} - - # 支持的工作流(静态预定义) - self.supported_workflows: Dict[str, WorkflowInfo] = {} - - # 初始化物料系统 - self._initialize_material_system() - - # 注册支持的工作流 - # self._register_supported_workflows() - - # logger.info(f"工作站 {device_id} 初始化完成(简化版)") - - def _initialize_material_system(self): - """初始化物料系统 - 使用 graphio 转换""" - try: - from unilabos.resources.graphio import resource_ulab_to_plr - - # # 1. 合并 deck_config 和 children 创建完整的资源树 - # complete_resource_config = self._create_complete_resource_config() - - # # 2. 使用 graphio 转换为 PLR 资源 - # self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True) - - # # 3. 建立资源映射 - # self._build_resource_mappings(self.deck) - - # # 4. 如果有资源同步器,执行初始同步 - # if self.resource_synchronizer: - # # 这里可以异步执行,暂时跳过 - # pass - - # logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源") - pass - except Exception as e: - # logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}") - raise - - def _create_complete_resource_config(self) -> Dict[str, Any]: - """创建完整的资源配置 - 合并 deck_config 和 children""" - # 创建主 deck 配置 - deck_resource = { - "id": f"{self.device_id}_deck", - "name": f"{self.device_id}_deck", - "type": "deck", - "position": {"x": 0, "y": 0, "z": 0}, - "config": { - "size_x": self.deck_config.get("size_x", 1000.0), - "size_y": self.deck_config.get("size_y", 1000.0), - "size_z": self.deck_config.get("size_z", 100.0), - **{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]}, - }, - "data": {}, - "children": [], - "parent": None, - } - - # 添加子资源 - if self._children: - children_list = [] - for child_id, child_config in self._children.items(): - child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"]) - children_list.append(child_resource) - deck_resource["children"] = children_list - - return deck_resource - - def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]: - """标准化子资源配置""" - return { - "id": resource_id, - "name": config.get("name", resource_id), - "type": config.get("type", "container"), - "position": self._normalize_position(config.get("position", {})), - "config": config.get("config", {}), - "data": config.get("data", {}), - "children": [], # 简化版本:只支持一层子资源 - "parent": parent_id, - } - - def _normalize_position(self, position: Any) -> Dict[str, float]: - """标准化位置信息""" - if isinstance(position, dict): - return { - "x": float(position.get("x", 0)), - "y": float(position.get("y", 0)), - "z": float(position.get("z", 0)), - } - elif isinstance(position, (list, tuple)) and len(position) >= 2: - return { - "x": float(position[0]), - "y": float(position[1]), - "z": float(position[2]) if len(position) > 2 else 0.0, - } - else: - return {"x": 0.0, "y": 0.0, "z": 0.0} - - def _build_resource_mappings(self, deck: Deck): - """递归构建资源映射""" - - def add_resource_recursive(resource: PLRResource): - if hasattr(resource, "name"): - self.plr_resources[resource.name] = resource - - if hasattr(resource, "children"): - for child in resource.children: - add_resource_recursive(child) - - add_resource_recursive(deck) - - # ============ 硬件接口管理 ============ - - def set_hardware_interface(self, hardware_interface: Union[Any, str]): - """设置硬件接口""" - self.hardware_interface = hardware_interface - logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}") - - def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"): - """设置协议节点引用(用于代理模式)""" - self._ros_node = workstation_node - logger.info(f"工作站 {self.device_id} 关联协议节点") - - # ============ 设备操作接口 ============ - - def call_device_method(self, method: str, *args, **kwargs) -> Any: - """调用设备方法的统一接口""" - # 1. 代理模式:通过协议节点转发 - if isinstance(self.hardware_interface, str) and self.hardware_interface.startswith("proxy:"): - if not self._ros_node: - raise RuntimeError("代理模式需要设置workstation_node") - - device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀 - return self._ros_node.call_device_method(device_id, method, *args, **kwargs) - - # 2. 直接模式:直接调用硬件接口方法 - elif self.hardware_interface and hasattr(self.hardware_interface, method): - return getattr(self.hardware_interface, method)(*args, **kwargs) - - else: - raise AttributeError(f"硬件接口不支持方法: {method}") - - def get_device_status(self) -> Dict[str, Any]: - """获取设备状态""" - try: - return self.call_device_method("get_status") - except AttributeError: - # 如果设备不支持get_status方法,返回基础状态 - return { - "status": "unknown", - "interface_type": type(self.hardware_interface).__name__, - "timestamp": time.time(), - } - - def is_device_available(self) -> bool: - """检查设备是否可用""" - try: - self.get_device_status() - return True - except: - return False - - # ============ 物料系统接口 ============ - - def get_deck(self) -> Deck: - """获取主 Deck""" - return self.deck - - def get_all_resources(self) -> Dict[str, PLRResource]: - """获取所有 PLR 资源""" - return self.plr_resources.copy() - - def find_resource_by_name(self, name: str) -> Optional[PLRResource]: - """按名称查找资源""" - return self.plr_resources.get(name) - - def find_resources_by_type(self, resource_type: type) -> List[PLRResource]: - """按类型查找资源""" - return [res for res in self.plr_resources.values() if isinstance(res, resource_type)] - - async def sync_with_external_system(self) -> bool: - """与外部物料系统同步""" - if not self.resource_synchronizer: - logger.info(f"工作站 {self.device_id} 没有配置资源同步器") - return True - - try: - success = await self.resource_synchronizer.sync_from_external() - if success: - logger.info(f"工作站 {self.device_id} 外部同步成功") - else: - logger.warning(f"工作站 {self.device_id} 外部同步失败") - return success - except Exception as e: - logger.error(f"工作站 {self.device_id} 外部同步异常: {e}") - return False - - # ============ 简化的工作流控制 ============ - - def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: - """执行工作流""" - try: - # 设置工作流状态 - self.current_workflow_status = WorkflowStatus.INITIALIZING - self.workflow_parameters = parameters - self.workflow_start_time = time.time() - - # 委托给子类实现 - success = self._execute_workflow_impl(workflow_name, parameters) - - if success: - self.current_workflow_status = WorkflowStatus.RUNNING - logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功") - else: - self.current_workflow_status = WorkflowStatus.ERROR - logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败") - - return success - - except Exception as e: - self.current_workflow_status = WorkflowStatus.ERROR - logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}") - return False - - def stop_workflow(self, emergency: bool = False) -> bool: - """停止工作流""" - try: - if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]: - logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流") - return True - - self.current_workflow_status = WorkflowStatus.STOPPING - - # 委托给子类实现 - success = self._stop_workflow_impl(emergency) - - if success: - self.current_workflow_status = WorkflowStatus.STOPPED - logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})") - else: - self.current_workflow_status = WorkflowStatus.ERROR - logger.error(f"工作站 {self.device_id} 工作流停止失败") - - return success - - except Exception as e: - self.current_workflow_status = WorkflowStatus.ERROR - logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}") - return False - - # ============ 状态属性 ============ - - @property - def workflow_status(self) -> WorkflowStatus: - """获取当前工作流状态""" - return self.current_workflow_status - - @property - def is_busy(self) -> bool: - """检查工作站是否忙碌""" - return self.current_workflow_status in [ - WorkflowStatus.INITIALIZING, - WorkflowStatus.RUNNING, - WorkflowStatus.STOPPING, - ] - - @property - def workflow_runtime(self) -> float: - """获取工作流运行时间(秒)""" - if self.workflow_start_time is None: - return 0.0 - return time.time() - self.workflow_start_time - - # ============ 抽象方法 - 子类必须实现 ============ - - # @abstractmethod - # def _register_supported_workflows(self): - # """注册支持的工作流 - 子类必须实现""" - # pass - - # @abstractmethod - # def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: - # """执行工作流的具体实现 - 子类必须实现""" - # pass - - # @abstractmethod - # def _stop_workflow_impl(self, emergency: bool = False) -> bool: - # """停止工作流的具体实现 - 子类必须实现""" - # pass - -class WorkstationExample(WorkstationBase): - """工作站示例实现""" - - def _register_supported_workflows(self): - """注册支持的工作流""" - self.supported_workflows["example_workflow"] = WorkflowInfo( - name="example_workflow", - description="这是一个示例工作流", - estimated_duration=300.0, - required_materials=["sample_plate"], - output_product="processed_plate", - parameters_schema={"param1": "string", "param2": "integer"}, - ) - - def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: - """执行工作流的具体实现""" - if workflow_name not in self.supported_workflows: - logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}") - return False - - # 这里添加实际的工作流逻辑 - logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}") - return True - - def _stop_workflow_impl(self, emergency: bool = False) -> bool: - """停止工作流的具体实现""" - # 这里添加实际的停止逻辑 - logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})") - return True \ No newline at end of file From 6d7c39da9e9e5ddb2b1727aec4bfa0a8055d3d64 Mon Sep 17 00:00:00 2001 From: lixinyu1011 <674842481@qq.com> Date: Fri, 31 Oct 2025 15:29:59 +0800 Subject: [PATCH 2/4] 1031 --- test/resources/test.json | 191 ++++++++++++++++++ .../bioyond_cell/bioyond_cell_workstation.py | 132 +++++++++++- .../workstation/bioyond_studio/config.py | 2 +- unilabos/registry/devices/laiyu_liquid.yaml | 11 +- unilabos/registry/devices/liquid_handler.yaml | 33 ++- 5 files changed, 333 insertions(+), 36 deletions(-) create mode 100644 test/resources/test.json diff --git a/test/resources/test.json b/test/resources/test.json new file mode 100644 index 0000000..9fa9237 --- /dev/null +++ b/test/resources/test.json @@ -0,0 +1,191 @@ +{ + "data": [ + { + "orderCode": "BSO2025103100006", + "orderName": "DP20250927001", + "errorMessage": null, + "usedMaterials": [ + { + "id": "3a1d4b13-25a6-cfb2-7315-159f14b32425", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b13-2467-e64d-d8bc-3957fb6e3240", + "materialName": "适配器块", + "materialCode": "0018-00065", + "quantity": "1块", + "materialTypeId": "efc3bb32-d504-4890-91c0-b64ed3ac80cf", + "materialTypeCode": "0018", + "materialTypeMode": "Consumables", + "materialTypeName": "适配器块", + "locationId": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c", + "locationCode": "0014-0001", + "locationShowName": "0014-0001" + }, + { + "id": "3a1d4b13-2420-8cfe-17f1-5f77a6ff6dc3", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b11-e448-bf90-d0bd-b20758425370", + "materialName": "test1", + "materialCode": "0001-00063", + "quantity": "1块", + "materialTypeId": "3a190c8b-3284-af78-d29f-9a69463ad047", + "materialTypeCode": "0001", + "materialTypeMode": "Sample", + "materialTypeName": "配液瓶(小)板", + "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "locationCode": "4", + "locationShowName": "4" + }, + { + "id": "3a1d4b13-2420-73a1-2b4d-7bf6dd993c36", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b11-e448-fea3-8291-0b66ecd06d72", + "materialName": "test1", + "materialCode": "0002-00282", + "quantity": "1块", + "materialTypeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "materialTypeCode": "0002", + "materialTypeMode": "Sample", + "materialTypeName": "配液瓶(小)", + "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "locationCode": "4-1/1", + "locationShowName": "4-1/1" + }, + { + "id": "3a1d4b13-2420-e45f-192d-639887ad73b7", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b12-67fc-5f91-13ed-c223d0155399", + "materialName": "test2", + "materialCode": "0010-00059", + "quantity": "1块", + "materialTypeId": "3a192fa4-007d-ec7b-456e-2a8be7a13f23", + "materialTypeCode": "0010", + "materialTypeMode": "Sample", + "materialTypeName": "5ml分液瓶板", + "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "locationCode": "5", + "locationShowName": "5" + }, + { + "id": "3a1d4b13-2420-c052-93cc-002f0aae79fc", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b12-67fc-60f7-1129-3d1ef2a2d1f8", + "materialName": "test2", + "materialCode": "0007-00211", + "quantity": "1块", + "materialTypeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4", + "materialTypeCode": "0007", + "materialTypeMode": "Sample", + "materialTypeName": "5ml分液瓶", + "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "locationCode": "5-1/1", + "locationShowName": "5-1/1" + } + ] + }, + { + "orderCode": "BSO2025103100007", + "orderName": "DP20250927002", + "errorMessage": null, + "usedMaterials": [ + { + "id": "3a1d4b13-264b-aca7-9e97-ab4df186d5c2", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b13-2467-e64d-d8bc-3957fb6e3240", + "materialName": "适配器块", + "materialCode": "0018-00065", + "quantity": "1块", + "materialTypeId": "efc3bb32-d504-4890-91c0-b64ed3ac80cf", + "materialTypeCode": "0018", + "materialTypeMode": "Consumables", + "materialTypeName": "适配器块", + "locationId": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c", + "locationCode": "0014-0001", + "locationShowName": "0014-0001" + }, + { + "id": "3a1d4b13-263e-873e-1331-7e668b411e98", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b11-e448-bf90-d0bd-b20758425370", + "materialName": "test1", + "materialCode": "0001-00063", + "quantity": "1块", + "materialTypeId": "3a190c8b-3284-af78-d29f-9a69463ad047", + "materialTypeCode": "0001", + "materialTypeMode": "Sample", + "materialTypeName": "配液瓶(小)板", + "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "locationCode": "4", + "locationShowName": "4" + }, + { + "id": "3a1d4b13-263e-7884-d9e0-b010478b7448", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b11-e448-82e0-6a64-6230ee1bf0a9", + "materialName": "test1", + "materialCode": "0002-00283", + "quantity": "1块", + "materialTypeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "materialTypeCode": "0002", + "materialTypeMode": "Sample", + "materialTypeName": "配液瓶(小)", + "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "locationCode": "4-1/2", + "locationShowName": "4-1/2" + }, + { + "id": "3a1d4b13-263e-6e99-b513-66047191643f", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b12-67fc-5f91-13ed-c223d0155399", + "materialName": "test2", + "materialCode": "0010-00059", + "quantity": "1块", + "materialTypeId": "3a192fa4-007d-ec7b-456e-2a8be7a13f23", + "materialTypeCode": "0010", + "materialTypeMode": "Sample", + "materialTypeName": "5ml分液瓶板", + "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "locationCode": "5", + "locationShowName": "5" + }, + { + "id": "3a1d4b13-263e-5b21-2c41-53e4ea7fe947", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b12-67fc-131a-82ff-87e9e7708f9f", + "materialName": "test2", + "materialCode": "0007-00212", + "quantity": "1块", + "materialTypeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4", + "materialTypeCode": "0007", + "materialTypeMode": "Sample", + "materialTypeName": "5ml分液瓶", + "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "locationCode": "5-1/2", + "locationShowName": "5-1/2" + } + ] + } + ], + "code": 1, + "message": "", + "timestamp": 1761891208109 +} + +25-10-31 [14:27:52,203] [ERROR] 从Bioyond同步物料数据失败: 'BottleCarrier' object has no attribute 'tracker' [sync_from_external:83] [unilabos.utils.log.station] +Traceback (most recent call last): + File "C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\station.py", line 73, in sync_from_external + unilab_resources = resource_bioyond_to_plr( + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\ML\GitHub\Uni-Lab-OS\unilabos\resources\graphio.py", line 661, in resource_bioyond_to_plr + bottle.tracker.liquids = [ + ^^^^^^^^^^^^^^ +AttributeError: 'BottleCarrier' object has no attribute 'tracker' \ No newline at end of file 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 5ce49e0..c40945e 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 @@ -253,7 +253,7 @@ class BioyondCellWorkstation(BioyondWorkstation): def auto_feeding4to3( self, # ★ 修改点:默认模板路径 - xlsx_path: Optional[str] = "/Users/calvincao/Desktop/work/uni-lab-all/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx", + xlsx_path: Optional[str] = "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, @@ -630,7 +630,12 @@ class BioyondCellWorkstation(BioyondWorkstation): response = self._post_lims("/api/lims/order/orders", orders) print(response) # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") + 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 @@ -963,6 +968,119 @@ class BioyondCellWorkstation(BioyondWorkstation): 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() + if not material_name: + raise ValueError("material_name 不能为空") + 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, + } + # -------------------------------- @@ -971,7 +1089,7 @@ if __name__ == "__main__": lab_registry.setup() ws = BioyondCellWorkstation() # logger.info(ws.scheduler_stop()) - # logger.info(ws.scheduler_start()) + logger.info(ws.scheduler_start()) # results = ws.create_materials(SOLID_LIQUID_MAPPINGS) # for r in results: @@ -980,11 +1098,11 @@ if __name__ == "__main__": # result = ws.create_and_inbound_materials() # 继续后续流程 - # logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱 + 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()) + 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()) diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py index 504cf45..519e686 100644 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ b/unilabos/devices/workstation/bioyond_studio/config.py @@ -16,7 +16,7 @@ API_CONFIG = { "report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"), # HTTP 服务配置 - "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.210"), # HTTP服务监听地址,监听计算机飞连ip地址 + "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.33.174"), # HTTP服务监听地址,监听计算机飞连ip地址 "HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")), "debug_mode": False,# 调试模式 } diff --git a/unilabos/registry/devices/laiyu_liquid.yaml b/unilabos/registry/devices/laiyu_liquid.yaml index 98201a7..64c0c18 100644 --- a/unilabos/registry/devices/laiyu_liquid.yaml +++ b/unilabos/registry/devices/laiyu_liquid.yaml @@ -1361,8 +1361,7 @@ laiyu_liquid: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -1492,11 +1491,9 @@ laiyu_liquid: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index b21ccd7..99c9233 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -3994,8 +3994,7 @@ liquid_handler: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -4151,11 +4150,9 @@ liquid_handler: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 @@ -5015,8 +5012,7 @@ liquid_handler.biomek: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -5159,11 +5155,9 @@ liquid_handler.biomek: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 @@ -7807,8 +7801,7 @@ liquid_handler.prcxi: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -7937,11 +7930,9 @@ liquid_handler.prcxi: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 From a62896eda298eccd61f971c0fb2e5b835584331b Mon Sep 17 00:00:00 2001 From: lixinyu1011 <674842481@qq.com> Date: Fri, 31 Oct 2025 18:57:38 +0800 Subject: [PATCH 3/4] 1031_byxinyu --- test/resources/test copy.json | 99 ++++++ test/resources/test.json | 305 +++++++----------- .../bioyond_cell/bioyond_cell_workstation.py | 73 ++++- .../bioyond_cell/material_template.xlsx | Bin 22168 -> 22207 bytes 4 files changed, 276 insertions(+), 201 deletions(-) create mode 100644 test/resources/test copy.json diff --git a/test/resources/test copy.json b/test/resources/test copy.json new file mode 100644 index 0000000..f9e9aa0 --- /dev/null +++ b/test/resources/test copy.json @@ -0,0 +1,99 @@ + { + "typeId": "3a190c8b-3284-af78-d29f-9a69463ad047", + "code": "", + "barCode": "", + "name": "test", + "unit": "", + "parameters": "{}", + "quantity": "", + "details": [ + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)11", + "quantity": "1", + "x": 1, + "y": 1, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)21", + "quantity": "1", + "x": 2, + "y": 1, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)12", + "quantity": "1", + "x": 1, + "y": 2, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)22", + "quantity": "1", + "x": 2, + "y": 2, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)13", + "quantity": "1", + "x": 1, + "y": 3, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)23", + "quantity": "1", + "x": 2, + "y": 3, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)14", + "quantity": "1", + "x": 1, + "y": 4, + "z": 1, + "unit": "", + "parameters": "{}" + }, + { + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "code": "", + "name": "配液瓶(小)24", + "quantity": "1", + "x": 2, + "y": 4, + "z": 1, + "unit": "", + "parameters": "{}" + } + ] + } \ No newline at end of file diff --git a/test/resources/test.json b/test/resources/test.json index 9fa9237..ee1be0f 100644 --- a/test/resources/test.json +++ b/test/resources/test.json @@ -1,191 +1,114 @@ -{ - "data": [ - { - "orderCode": "BSO2025103100006", - "orderName": "DP20250927001", - "errorMessage": null, - "usedMaterials": [ - { - "id": "3a1d4b13-25a6-cfb2-7315-159f14b32425", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", - "materialId": "3a1d4b13-2467-e64d-d8bc-3957fb6e3240", - "materialName": "适配器块", - "materialCode": "0018-00065", - "quantity": "1块", - "materialTypeId": "efc3bb32-d504-4890-91c0-b64ed3ac80cf", - "materialTypeCode": "0018", - "materialTypeMode": "Consumables", - "materialTypeName": "适配器块", - "locationId": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c", - "locationCode": "0014-0001", - "locationShowName": "0014-0001" - }, - { - "id": "3a1d4b13-2420-8cfe-17f1-5f77a6ff6dc3", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", - "materialId": "3a1d4b11-e448-bf90-d0bd-b20758425370", - "materialName": "test1", - "materialCode": "0001-00063", - "quantity": "1块", - "materialTypeId": "3a190c8b-3284-af78-d29f-9a69463ad047", - "materialTypeCode": "0001", - "materialTypeMode": "Sample", - "materialTypeName": "配液瓶(小)板", - "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", - "locationCode": "4", - "locationShowName": "4" - }, - { - "id": "3a1d4b13-2420-73a1-2b4d-7bf6dd993c36", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", - "materialId": "3a1d4b11-e448-fea3-8291-0b66ecd06d72", - "materialName": "test1", - "materialCode": "0002-00282", - "quantity": "1块", - "materialTypeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", - "materialTypeCode": "0002", - "materialTypeMode": "Sample", - "materialTypeName": "配液瓶(小)", - "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", - "locationCode": "4-1/1", - "locationShowName": "4-1/1" - }, - { - "id": "3a1d4b13-2420-e45f-192d-639887ad73b7", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", - "materialId": "3a1d4b12-67fc-5f91-13ed-c223d0155399", - "materialName": "test2", - "materialCode": "0010-00059", - "quantity": "1块", - "materialTypeId": "3a192fa4-007d-ec7b-456e-2a8be7a13f23", - "materialTypeCode": "0010", - "materialTypeMode": "Sample", - "materialTypeName": "5ml分液瓶板", - "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", - "locationCode": "5", - "locationShowName": "5" - }, - { - "id": "3a1d4b13-2420-c052-93cc-002f0aae79fc", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", - "materialId": "3a1d4b12-67fc-60f7-1129-3d1ef2a2d1f8", - "materialName": "test2", - "materialCode": "0007-00211", - "quantity": "1块", - "materialTypeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4", - "materialTypeCode": "0007", - "materialTypeMode": "Sample", - "materialTypeName": "5ml分液瓶", - "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", - "locationCode": "5-1/1", - "locationShowName": "5-1/1" - } - ] - }, - { - "orderCode": "BSO2025103100007", - "orderName": "DP20250927002", - "errorMessage": null, - "usedMaterials": [ - { - "id": "3a1d4b13-264b-aca7-9e97-ab4df186d5c2", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", - "materialId": "3a1d4b13-2467-e64d-d8bc-3957fb6e3240", - "materialName": "适配器块", - "materialCode": "0018-00065", - "quantity": "1块", - "materialTypeId": "efc3bb32-d504-4890-91c0-b64ed3ac80cf", - "materialTypeCode": "0018", - "materialTypeMode": "Consumables", - "materialTypeName": "适配器块", - "locationId": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c", - "locationCode": "0014-0001", - "locationShowName": "0014-0001" - }, - { - "id": "3a1d4b13-263e-873e-1331-7e668b411e98", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", - "materialId": "3a1d4b11-e448-bf90-d0bd-b20758425370", - "materialName": "test1", - "materialCode": "0001-00063", - "quantity": "1块", - "materialTypeId": "3a190c8b-3284-af78-d29f-9a69463ad047", - "materialTypeCode": "0001", - "materialTypeMode": "Sample", - "materialTypeName": "配液瓶(小)板", - "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", - "locationCode": "4", - "locationShowName": "4" - }, - { - "id": "3a1d4b13-263e-7884-d9e0-b010478b7448", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", - "materialId": "3a1d4b11-e448-82e0-6a64-6230ee1bf0a9", - "materialName": "test1", - "materialCode": "0002-00283", - "quantity": "1块", - "materialTypeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", - "materialTypeCode": "0002", - "materialTypeMode": "Sample", - "materialTypeName": "配液瓶(小)", - "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", - "locationCode": "4-1/2", - "locationShowName": "4-1/2" - }, - { - "id": "3a1d4b13-263e-6e99-b513-66047191643f", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", - "materialId": "3a1d4b12-67fc-5f91-13ed-c223d0155399", - "materialName": "test2", - "materialCode": "0010-00059", - "quantity": "1块", - "materialTypeId": "3a192fa4-007d-ec7b-456e-2a8be7a13f23", - "materialTypeCode": "0010", - "materialTypeMode": "Sample", - "materialTypeName": "5ml分液瓶板", - "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", - "locationCode": "5", - "locationShowName": "5" - }, - { - "id": "3a1d4b13-263e-5b21-2c41-53e4ea7fe947", - "destinationType": "TempOrder", - "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", - "materialId": "3a1d4b12-67fc-131a-82ff-87e9e7708f9f", - "materialName": "test2", - "materialCode": "0007-00212", - "quantity": "1块", - "materialTypeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4", - "materialTypeCode": "0007", - "materialTypeMode": "Sample", - "materialTypeName": "5ml分液瓶", - "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", - "locationCode": "5-1/2", - "locationShowName": "5-1/2" - } - ] - } - ], - "code": 1, - "message": "", - "timestamp": 1761891208109 -} - -25-10-31 [14:27:52,203] [ERROR] 从Bioyond同步物料数据失败: 'BottleCarrier' object has no attribute 'tracker' [sync_from_external:83] [unilabos.utils.log.station] -Traceback (most recent call last): - File "C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\station.py", line 73, in sync_from_external - unilab_resources = resource_bioyond_to_plr( - ^^^^^^^^^^^^^^^^^^^^^^^^ - File "C:\ML\GitHub\Uni-Lab-OS\unilabos\resources\graphio.py", line 661, in resource_bioyond_to_plr - bottle.tracker.liquids = [ - ^^^^^^^^^^^^^^ -AttributeError: 'BottleCarrier' object has no attribute 'tracker' \ No newline at end of file +[ + { + "id": "3a1d4b7e-4bdc-16bf-7169-f60350d03c7e", + "typeName": "配液瓶(小)板", + "code": "0001-00088", + "barCode": "", + "name": "test1", + "quantity": 1.0, + "lockQuantity": 0.0, + "unit": "块", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "whid": "3a19deae-2c79-05a3-9c76-8e6760424841", + "whName": "手动堆栈", + "code": "4", + "x": 2, + "y": 1, + "z": 1, + "quantity": 0 + } + ], + "detail": [ + { + "id": "3a1d4b7e-4bdc-12e8-4d26-dddc77b03f63", + "detailMaterialId": "3a1d4b7e-4bdc-4e9e-8a3c-e9ba4a26457e", + "code": null, + "name": "test1", + "quantity": "1", + "lockQuantity": "0", + "unit": "块", + "x": 1, + "y": 2, + "z": 1, + "associateId": null, + "typeName": "配液瓶(小)", + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb" + }, + { + "id": "3a1d4b7e-4bdc-35b6-22d4-e6f3235e1c27", + "detailMaterialId": "3a1d4b7e-4bdc-ce0f-1fbb-b88de76fce98", + "code": null, + "name": "test1", + "quantity": "1", + "lockQuantity": "0", + "unit": "块", + "x": 1, + "y": 1, + "z": 1, + "associateId": null, + "typeName": "配液瓶(小)", + "typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb" + } + ] + }, + { + "id": "3a1d4b7e-ee61-ae87-9cd0-31c7e6621b18", + "typeName": "5ml分液瓶板", + "code": "0010-00089", + "barCode": "", + "name": "test2", + "quantity": 1.0, + "lockQuantity": 0.0, + "unit": "块", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "whid": "3a19deae-2c79-05a3-9c76-8e6760424841", + "whName": "手动堆栈", + "code": "5", + "x": 2, + "y": 2, + "z": 1, + "quantity": 0 + } + ], + "detail": [ + { + "id": "3a1d4b7e-ee61-8fb3-9a39-2c2841c3c8d0", + "detailMaterialId": "3a1d4b7e-ee61-305c-fe30-2620017ca1bd", + "code": null, + "name": "test2", + "quantity": "1", + "lockQuantity": "0", + "unit": "块", + "x": 1, + "y": 1, + "z": 1, + "associateId": null, + "typeName": "5ml分液瓶", + "typeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4" + }, + { + "id": "3a1d4b7e-ee61-ef5f-a7d1-f9399a4d3145", + "detailMaterialId": "3a1d4b7e-ee61-2f1d-6969-202ad3cbe226", + "code": null, + "name": "test2", + "quantity": "1", + "lockQuantity": "0", + "unit": "块", + "x": 1, + "y": 2, + "z": 1, + "associateId": null, + "typeName": "5ml分液瓶", + "typeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4" + } + ] + } +] \ No newline at end of file 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 c40945e..092a87f 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 @@ -968,7 +968,7 @@ class BioyondCellWorkstation(BioyondWorkstation): logger.error(f"✗ 执行失败: {e}") return {"success": False, "error": str(e)} -def create_material( + def create_material( self, material_name: str, type_id: str, @@ -985,8 +985,7 @@ def create_material( 包含创建结果、物料ID以及入库结果的字典。 """ material_name = (material_name or "").strip() - if not material_name: - raise ValueError("material_name 不能为空") + resolved_type_id = (type_id or "").strip() # 优先从 SOLID_LIQUID_MAPPINGS 中获取模板数据 template = SOLID_LIQUID_MAPPINGS.get(material_name) @@ -1082,14 +1081,68 @@ def create_material( } -# -------------------------------- + def create_sample( + self, + name: str, + board_type: str, + bottle_type: str, + location_code: str + ) -> Dict[str, Any]: + """创建配液板物料并自动入库。 + Args: + material_name: 物料名称,支持 "5ml分液瓶板"/"5ml分液瓶"、"配液瓶(小)板"/"配液瓶(小)"。 + quantity: 主物料与明细的数量,默认 1。 + location_code: 库位编号,例如 "A01",将自动映射为 "手动堆栈" 下的 UUID。 + """ + carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1] + bottle_type_id = MATERIAL_TYPE_MAPPINGS[bottle_type][1] + location_id = WAREHOUSE_MAPPING["手动堆栈"]["site_uuids"][location_code] + + # 新建小瓶 + 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() ws = BioyondCellWorkstation() + 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.scheduler_start()) # results = ws.create_materials(SOLID_LIQUID_MAPPINGS) # for r in results: @@ -1098,11 +1151,11 @@ if __name__ == "__main__": # result = ws.create_and_inbound_materials() # 继续后续流程 - logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱 - # # 使用正斜杠或 Path 对象来指定文件路径 - excel_path = Path("unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\2025092701.xlsx") - logger.info(ws.create_orders(excel_path)) - logger.info(ws.transfer_3_to_2_to_1()) + # logger.info(ws.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()) 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 844fc84d932f618891abbdf6efdefc4bcf16289f..abaf145e68e8910ddf877637d20ff10748c489ad 100644 GIT binary patch delta 109 zcmbQSmT~`D#tr?d0{b^wimsPpU;tqW1_p-727wZrx2viNO%~8*V=^+ETx_Ac`L+26 z8BVy$$xlMU8Pg`)gxWAkyG*VMwPx&^ye-s%@&Dxap|*_ilXb)F7+*}z2~%U!3jvzQ F006IYBS-)M delta 100 zcmdnLmT|^f#tr?de40E$H#?*l7(iHpVX|DP)aLD~YC@Cit$8;KT6~n@ge#cL9U9JP vKRG$nhUts* Date: Fri, 31 Oct 2025 19:02:06 +0800 Subject: [PATCH 4/4] Delete button_battery_station.py --- .../button_battery_station.py | 1006 ----------------- 1 file changed, 1006 deletions(-) delete mode 100644 unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py diff --git a/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py b/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py deleted file mode 100644 index eae09b8..0000000 --- a/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py +++ /dev/null @@ -1,1006 +0,0 @@ -""" -纽扣电池组装工作站物料类定义 -Button Battery Assembly Station Resource Classes -""" - -from __future__ import annotations - -from collections import OrderedDict -from typing import Any, Dict, List, Optional, TypedDict, Union, cast - -from pylabrobot.resources.coordinate import Coordinate -from pylabrobot.resources.container import Container -from pylabrobot.resources.deck import Deck -from pylabrobot.resources.itemized_resource import ItemizedResource -from pylabrobot.resources.resource import Resource -from pylabrobot.resources.resource_stack import ResourceStack -from pylabrobot.resources.tip_rack import TipRack, TipSpot -from pylabrobot.resources.trash import Trash -from pylabrobot.resources.utils import create_ordered_items_2d - - -class ElectrodeSheetState(TypedDict): - diameter: float # 直径 (mm) - thickness: float # 厚度 (mm) - mass: float # 质量 (g) - material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等) - height: float - electrolyte_name: str - data_electrolyte_code: str - open_circuit_voltage: float - assembly_pressure: float - electrolyte_volume: float - - info: Optional[str] # 附加信息 - -class ElectrodeSheet(Resource): - """极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料""" - - def __init__( - self, - name: str = "极片", - size_x=10, - size_y=10, - size_z=10, - category: str = "electrode_sheet", - model: Optional[str] = None, - ): - """初始化极片 - - Args: - name: 极片名称 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - model=model, - ) - self._unilabos_state: ElectrodeSheetState = ElectrodeSheetState( - diameter=14, - thickness=0.1, - mass=0.5, - material_type="copper", - info=None - ) - - # TODO: 这个还要不要?给self._unilabos_state赋值的? - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - #序列化 - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - -# TODO: 这个应该只能放一个极片 -class MaterialHoleState(TypedDict): - diameter: int - depth: int - max_sheets: int - info: Optional[str] # 附加信息 - -class MaterialHole(Resource): - """料板洞位类""" - children: List[ElectrodeSheet] = [] - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - category: str = "material_hole", - **kwargs - ): - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - ) - self._unilabos_state: MaterialHoleState = MaterialHoleState( - diameter=20, - depth=10, - max_sheets=1, - info=None - ) - - def get_all_sheet_info(self): - info_list = [] - for sheet in self.children: - info_list.append(sheet._unilabos_state["info"]) - return info_list - - #这个函数函数好像没用,一般不会集中赋值质量 - def set_all_sheet_mass(self): - for sheet in self.children: - sheet._unilabos_state["mass"] = 0.5 # 示例:设置质量为0.5g - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - #移动极片前先取出对象 - def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]: - for sheet in self.children: - if sheet.name == name: - return sheet - return None - - def has_electrode_sheet(self) -> bool: - """检查洞位是否有极片""" - return len(self.children) > 0 - - def assign_child_resource( - self, - resource: ElectrodeSheet, - location: Optional[Coordinate], - reassign: bool = True, - ): - """放置极片""" - # TODO: 这里要改,diameter找不到,加入._unilabos_state后应该没问题 - #if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]: - # raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}") - #if len(self.children) >= self._unilabos_state["max_sheets"]: - # raise ValueError(f"洞位已满,无法放置更多极片") - super().assign_child_resource(resource, location, reassign) - - # 根据children的编号取物料对象。 - def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet: - return self.children[index] - - - -class MaterialPlateState(TypedDict): - hole_spacing_x: float - hole_spacing_y: float - hole_diameter: float - info: Optional[str] # 附加信息 - -class MaterialPlate(ItemizedResource[MaterialHole]): - """料板类 - 4x4个洞位,每个洞位放1个极片""" - - children: List[MaterialHole] - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - ordered_items: Optional[Dict[str, MaterialHole]] = None, - ordering: Optional[OrderedDict[str, str]] = None, - category: str = "material_plate", - model: Optional[str] = None, - fill: bool = False - ): - """初始化料板 - - Args: - name: 料板名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - hole_diameter: 洞直径 (mm) - hole_depth: 洞深度 (mm) - hole_spacing_x: X方向洞位间距 (mm) - hole_spacing_y: Y方向洞位间距 (mm) - number: 编号 - category: 类别 - model: 型号 - """ - self._unilabos_state: MaterialPlateState = MaterialPlateState( - hole_spacing_x=24.0, - hole_spacing_y=24.0, - hole_diameter=20.0, - info="", - ) - # 创建4x4的洞位 - # TODO: 这里要改,对应不同形状 - holes = create_ordered_items_2d( - klass=MaterialHole, - num_items_x=4, - num_items_y=4, - dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中 - dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中 - dz=size_z, - item_dx=self._unilabos_state["hole_spacing_x"], - item_dy=self._unilabos_state["hole_spacing_y"], - size_x = 16, - size_y = 16, - size_z = 16, - ) - if fill: - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=holes, - category=category, - model=model, - ) - else: - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=ordered_items, - ordering=ordering, - category=category, - model=model, - ) - - def update_locations(self): - # TODO:调多次相加 - holes = create_ordered_items_2d( - klass=MaterialHole, - num_items_x=4, - num_items_y=4, - dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中 - dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中 - dz=self._size_z, - item_dx=self._unilabos_state["hole_spacing_x"], - item_dy=self._unilabos_state["hole_spacing_y"], - size_x = 1, - size_y = 1, - size_z = 1, - ) - for item, original_item in zip(holes.items(), self.children): - original_item.location = item[1].location - - -class PlateSlot(ResourceStack): - """板槽位类 - 1个槽上能堆放8个板,移板只能操作最上方的板""" - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - max_plates: int = 8, - category: str = "plate_slot", - model: Optional[str] = None - ): - """初始化板槽位 - - Args: - name: 槽位名称 - max_plates: 最大板数量 - category: 类别 - """ - super().__init__( - name=name, - direction="z", # Z方向堆叠 - resources=[], - ) - self.max_plates = max_plates - self.category = category - - def can_add_plate(self) -> bool: - """检查是否可以添加板""" - return len(self.children) < self.max_plates - - def add_plate(self, plate: MaterialPlate) -> None: - """添加料板""" - if not self.can_add_plate(): - raise ValueError(f"槽位 {self.name} 已满,无法添加更多板") - self.assign_child_resource(plate) - - def get_top_plate(self) -> MaterialPlate: - """获取最上方的板""" - if len(self.children) == 0: - raise ValueError(f"槽位 {self.name} 为空") - return cast(MaterialPlate, self.get_top_item()) - - def take_top_plate(self) -> MaterialPlate: - """取出最上方的板""" - top_plate = self.get_top_plate() - self.unassign_child_resource(top_plate) - return top_plate - - def can_access_for_picking(self) -> bool: - """检查是否可以进行取料操作(只有最上方的板能进行取料操作)""" - return len(self.children) > 0 - - def serialize(self) -> dict: - return { - **super().serialize(), - "max_plates": self.max_plates, - } - - -class ClipMagazineHole(Container): - """子弹夹洞位类""" - - def __init__( - self, - name: str, - diameter: float, - depth: float, - max_sheets: int = 100, - category: str = "clip_magazine_hole", - ): - """初始化子弹夹洞位 - - Args: - name: 洞位名称 - diameter: 洞直径 (mm) - depth: 洞深度 (mm) - max_sheets: 最大极片数量 - category: 类别 - """ - super().__init__( - name=name, - size_x=diameter, - size_y=diameter, - size_z=depth, - category=category, - ) - self.diameter = diameter - self.depth = depth - self.max_sheets = max_sheets - self._sheets: List[ElectrodeSheet] = [] - - def can_add_sheet(self, sheet: ElectrodeSheet) -> bool: - """检查是否可以添加极片""" - return (len(self._sheets) < self.max_sheets and - sheet.diameter <= self.diameter) - - def add_sheet(self, sheet: ElectrodeSheet) -> None: - """添加极片""" - if not self.can_add_sheet(sheet): - raise ValueError(f"无法向洞位 {self.name} 添加极片") - self._sheets.append(sheet) - - def take_sheet(self) -> ElectrodeSheet: - """取出极片""" - if len(self._sheets) == 0: - raise ValueError(f"洞位 {self.name} 没有极片") - return self._sheets.pop() - - def get_sheet_count(self) -> int: - """获取极片数量""" - return len(self._sheets) - - def serialize_state(self) -> Dict[str, Any]: - return { - "sheet_count": len(self._sheets), - "sheets": [sheet.serialize() for sheet in self._sheets], - } - -# TODO: 这个要改 -class ClipMagazine(Resource): - """子弹夹类 - 有6个洞位,每个洞位放多个极片""" - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - hole_spacing: float = 25.0, - max_sheets_per_hole: int = 100, - category: str = "clip_magazine", - model: Optional[str] = None, - ): - """初始化子弹夹 - - Args: - name: 子弹夹名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - hole_diameter: 洞直径 (mm) - hole_depth: 洞深度 (mm) - hole_spacing: 洞位间距 (mm) - max_sheets_per_hole: 每个洞位最大极片数量 - category: 类别 - model: 型号 - """ - # 创建6个洞位,排成2x3布局 - holes = create_ordered_items_2d( - klass=ClipMagazineHole, - num_items_x=3, - num_items_y=2, - dx=(size_x - 2 * hole_spacing) / 2, # 居中 - dy=(size_y - hole_spacing) / 2, # 居中 - dz=size_z - 0, - item_dx=hole_spacing, - item_dy=hole_spacing, - diameter=0, - depth=0, - ) - - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=holes, - category=category, - model=model, - ) - - self.hole_diameter = hole_diameter - self.hole_depth = hole_depth - self.max_sheets_per_hole = max_sheets_per_hole - - def serialize(self) -> dict: - return { - **super().serialize(), - "hole_diameter": self.hole_diameter, - "hole_depth": self.hole_depth, - "max_sheets_per_hole": self.max_sheets_per_hole, - } -#是一种类型注解,不用self -class BatteryState(TypedDict): - """电池状态字典""" - diameter: float - height: float - assembly_pressure: float - electrolyte_volume: float - electrolyte_name: str - -class Battery(Resource): - """电池类 - 可容纳极片""" - children: List[ElectrodeSheet] = [] - - def __init__( - self, - name: str, - size_x=1, - size_y=1, - size_z=1, - category: str = "battery", - ): - """初始化电池 - - Args: - name: 电池名称 - diameter: 直径 (mm) - height: 高度 (mm) - max_volume: 最大容量 (μL) - barcode: 二维码编号 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=1, - size_y=1, - size_z=1, - category=category, - ) - self._unilabos_state: BatteryState = BatteryState( - diameter = 1.0, - height = 1.0, - assembly_pressure = 1.0, - electrolyte_volume = 1.0, - electrolyte_name = "DP001" - ) - - def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool: - to_add_name = bottle._unilabos_state["electrolyte_name"] - if bottle.aspirate_electrolyte(10): - if self.add_electrolyte(to_add_name, 10): - pass - else: - bottle._unilabos_state["electrolyte_volume"] += 10 - - def set_electrolyte(self, name: str, volume: float) -> None: - """设置电解液信息""" - self._unilabos_state["electrolyte_name"] = name - self._unilabos_state["electrolyte_volume"] = volume - #这个应该没用,不会有加了后再加的事情 - def add_electrolyte(self, name: str, volume: float) -> bool: - """添加电解液信息""" - if name != self._unilabos_state["electrolyte_name"]: - return False - self._unilabos_state["electrolyte_volume"] += volume - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - -# 电解液作为属性放进去 - -class BatteryPressSlotState(TypedDict): - """电池状态字典""" - diameter: float =20.0 - depth: float = 4.0 - -class BatteryPressSlot(Resource): - """电池压制槽类 - 设备,可容纳一个电池""" - children: List[Battery] = [] - - def __init__( - self, - name: str = "BatteryPressSlot", - category: str = "battery_press_slot", - ): - """初始化电池压制槽 - - Args: - name: 压制槽名称 - diameter: 直径 (mm) - depth: 深度 (mm) - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=10, - size_y=12, - size_z=13, - category=category, - ) - self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState() - - def has_battery(self) -> bool: - """检查是否有电池""" - return len(self.children) > 0 - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - def assign_child_resource( - self, - resource: Battery, - location: Optional[Coordinate], - reassign: bool = True, - ): - """放置极片""" - # TODO: 让高京看下槽位只有一个电池时是否这么写。 - if self.has_battery(): - raise ValueError(f"槽位已含有一个电池,无法再放置其他电池") - super().assign_child_resource(resource, location, reassign) - - # 根据children的编号取物料对象。 - def get_battery_info(self, index: int) -> Battery: - return self.children[0] - -# TODO:这个移液枪架子看一下从哪继承 -class TipBox64State(TypedDict): - """电池状态字典""" - tip_diameter: float = 5.0 - tip_length: float = 50.0 - with_tips: bool = True - -class TipBox64(TipRack): - """64孔枪头盒类""" - - children: List[TipSpot] = [] - def __init__( - self, - name: str, - size_x: float = 127.8, - size_y: float = 85.5, - size_z: float = 60.0, - category: str = "tip_box_64", - model: Optional[str] = None, - ): - """初始化64孔枪头盒 - - Args: - name: 枪头盒名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - tip_diameter: 枪头直径 (mm) - tip_length: 枪头长度 (mm) - category: 类别 - model: 型号 - with_tips: 是否带枪头 - """ - from pylabrobot.resources.tip import Tip - - # 创建8x8=64个枪头位 - def make_tip(): - return Tip( - has_filter=False, - total_tip_length=20.0, - maximal_volume=1000, # 1mL - fitting_depth=8.0, - ) - - tip_spots = create_ordered_items_2d( - klass=TipSpot, - num_items_x=8, - num_items_y=8, - dx=8.0, - dy=8.0, - dz=0.0, - item_dx=9.0, - item_dy=9.0, - size_x=10, - size_y=10, - size_z=0.0, - make_tip=make_tip, - ) - self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=tip_spots, - category=category, - model=model, - with_tips=True, - ) - - - -class WasteTipBoxstate(TypedDict): - """"废枪头盒状态字典""" - max_tips: int = 100 - tip_count: int = 0 - -#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断 -class WasteTipBox(Trash): - """废枪头盒类 - 100个枪头容量""" - - def __init__( - self, - name: str, - size_x: float = 127.8, - size_y: float = 85.5, - size_z: float = 60.0, - category: str = "waste_tip_box", - model: Optional[str] = None, - ): - """初始化废枪头盒 - - Args: - name: 废枪头盒名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - max_tips: 最大枪头容量 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - model=model, - ) - self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() - - def add_tip(self) -> None: - """添加废枪头""" - if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]: - raise ValueError(f"废枪头盒 {self.name} 已满") - self._unilabos_state["tip_count"] += 1 - - def get_tip_count(self) -> int: - """获取枪头数量""" - return self._unilabos_state["tip_count"] - - def empty(self) -> None: - """清空废枪头盒""" - self._unilabos_state["tip_count"] = 0 - - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - -class BottleRackState(TypedDict): - """ bottle_diameter: 瓶子直径 (mm) - bottle_height: 瓶子高度 (mm) - position_spacing: 位置间距 (mm)""" - bottle_diameter: float - bottle_height: float - name_to_index: dict - - - -class BottleRack(Resource): - """瓶架类 - 12个待配位置+12个已配位置""" - children: List[Bottle] = [] - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - category: str = "bottle_rack", - model: Optional[str] = None, - ): - """初始化瓶架 - - Args: - name: 瓶架名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - model=model, - ) - # TODO: 添加瓶位坐标映射 - self.index_to_pos = { - 0: Coordinate.zero(), - 1: Coordinate(x=1, y=2, z=3) # 添加 - } - self.name_to_index = {} - self.name_to_pos = {} - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - # TODO: 这里有些问题要重新写一下 - def assign_child_resource(self, resource: Bottle, location=Coordinate.zero(), reassign = True): - assert len(self.children) <= 12, "瓶架已满,无法添加更多瓶子" - index = len(self.children) - location = Coordinate(x=20 + (index % 4) * 15, y=20 + (index // 4) * 15, z=0) - self.name_to_pos[resource.name] = location - self.name_to_index[resource.name] = index - return super().assign_child_resource(resource, location, reassign) - - def assign_child_resource_by_index(self, resource: Bottle, index: int): - assert 0 <= index < 12, "无效的瓶子索引" - self.name_to_index[resource.name] = index - location = self.index_to_pos[index] - return super().assign_child_resource(resource, location) - - def unassign_child_resource(self, resource: Bottle): - super().unassign_child_resource(resource) - self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None) - - # def serialize(self): - # self.children.sort(key=lambda x: self.name_to_index.get(x.name, 0)) - # return super().serialize() - - -class BottleState(TypedDict): - diameter: float - height: float - electrolyte_name: str - electrolyte_volume: float - max_volume: float - -class Bottle(Resource): - """瓶子类 - 容纳电解液""" - - def __init__( - self, - name: str, - category: str = "bottle", - ): - """初始化瓶子 - - Args: - name: 瓶子名称 - diameter: 直径 (mm) - height: 高度 (mm) - max_volume: 最大体积 (μL) - barcode: 二维码 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=1, - size_y=1, - size_z=1, - category=category, - ) - self._unilabos_state: BottleState = BottleState() - - def aspirate_electrolyte(self, volume: float) -> bool: - current_volume = self._unilabos_state["electrolyte_volume"] - assert current_volume > volume, f"Cannot aspirate {volume}μL, only {current_volume}μL available." - self._unilabos_state["electrolyte_volume"] -= volume - return True - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - -class CoincellDeck(Deck): - """纽扣电池组装工作站台面类""" - - def __init__( - self, - name: str = "coin_cell_deck", - size_x: float = 1620.0, # 3.66m - size_y: float = 1270.0, # 1.23m - size_z: float = 500.0, - origin: Coordinate = Coordinate(0, 0, 0), - category: str = "coin_cell_deck", - ): - """初始化纽扣电池组装工作站台面 - - Args: - name: 台面名称 - size_x: 长度 (mm) - 3.66m - size_y: 宽度 (mm) - 1.23m - size_z: 高度 (mm) - origin: 原点坐标 - category: 类别 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - origin=origin, - category=category, - ) - -#if __name__ == "__main__": -# # 转移极片的测试代码 -# deck = CoincellDeck("coin_cell_deck") -# ban_cao_wei = PlateSlot("ban_cao_wei", max_plates=8) -# deck.assign_child_resource(ban_cao_wei, Coordinate(x=0, y=0, z=0)) -# -# plate_1 = MaterialPlate("plate_1", 1,1,1, fill=True) -# for i, hole in enumerate(plate_1.children): -# sheet = ElectrodeSheet(f"hole_{i}_sheet_1") -# sheet._unilabos_state = { -# "diameter": 14, -# "info": "NMC", -# "mass": 5.0, -# "material_type": "positive_electrode", -# "thickness": 0.1 -# } -# hole._unilabos_state = { -# "depth": 1.0, -# "diameter": 14, -# "info": "", -# "max_sheets": 1 -# } -# hole.assign_child_resource(sheet, Coordinate.zero()) -# plate_1._unilabos_state = { -# "hole_spacing_x": 20.0, -# "hole_spacing_y": 20.0, -# "hole_diameter": 5, -# "info": "这是第一块料板" -# } -# plate_1.update_locations() -# ban_cao_wei.assign_child_resource(plate_1, Coordinate.zero()) -# # zi_dan_jia = ClipMagazine("zi_dan_jia", 1, 1, 1) -# # deck.assign_child_resource(ban_cao_wei, Coordinate(x=200, y=200, z=0)) -# -# from unilabos.resources.graphio import * -# A = tree_to_list([resource_plr_to_ulab(deck)]) -# with open("test.json", "w") as f: -# json.dump(A, f) -# -# -#def get_plate_with_14mm_hole(name=""): -# plate = MaterialPlate(name=name) -# for i in range(4): -# for j in range(4): -# hole = MaterialHole(f"{i+1}x{j+1}") -# hole._unilabos_state["diameter"] = 14 -# hole._unilabos_state["max_sheets"] = 1 -# plate.assign_child_resource(hole) -# return plate - -import json - -if __name__ == "__main__": - #electrode1 = BatteryPressSlot() - #print(electrode1.get_size_x()) - #print(electrode1.get_size_y()) - #print(electrode1.get_size_z()) - #jipian = ElectrodeSheet() - #jipian._unilabos_state["diameter"] = 18 - #print(jipian.serialize()) - #print(jipian.serialize_state()) - - deck = CoincellDeck(size_x=1000, - size_y=1000, - size_z=900) - - #liaopan = TipBox64(name="liaopan") - - #创建一个4*4的物料板 - liaopan1 = MaterialPlate(name="liaopan1", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) - #把物料板放到桌子上 - deck.assign_child_resource(liaopan1, Coordinate(x=0, y=0, z=0)) - #创建一个极片 - for i in range(16): - jipian = ElectrodeSheet(name=f"jipian1_{i}", size_x= 12, size_y=12, size_z=0.1) - liaopan1.children[i].assign_child_resource(jipian, location=None) -# - #创建一个4*4的物料板 - liaopan2 = MaterialPlate(name="liaopan2", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) - #把物料板放到桌子上 - deck.assign_child_resource(liaopan2, Coordinate(x=500, y=0, z=0)) - - #创建一个4*4的物料板 - liaopan3 = MaterialPlate(name="电池料盘", size_x=120.8, size_y=160.5, size_z=10.0, fill=True) - #把物料板放到桌子上 - deck.assign_child_resource(liaopan3, Coordinate(x=100, y=100, z=0)) - - - - #liaopan.children[3].assign_child_resource(jipian, location=None) - print(deck) - - - from unilabos.resources.graphio import convert_resources_from_type - from unilabos.config.config import BasicConfig - BasicConfig.ak = "4d5ce6ae-7234-4639-834e-93899b9caf94" - BasicConfig.sk = "505d3b0a-620e-459a-9905-1efcffce382a" - from unilabos.app.web.client import http_client - - resources = convert_resources_from_type([deck], [Resource]) - json.dump({"nodes": resources, "links": []}, open("button_battery_station_resources_unilab.json", "w"), indent=2) - - - #print(resources) - http_client.remote_addr = "https://uni-lab.test.bohrium.com/api/v1" - - http_client.resource_add(resources) \ No newline at end of file