diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index ea8c8e77..95526f0c 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -902,28 +902,28 @@ def _notify_resource_tree(dev_id, act, item_list): async def _handle_request_restart(self, data: Dict[str, Any]): """ 处理重启请求 - + 当LabGo发送request_restart时,执行清理并触发重启 """ reason = data.get("reason", "unknown") delay = data.get("delay", 2) # 默认延迟2秒 logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s") - + # 发送确认消息 if self.websocket_client: await self.websocket_client.send_message({ "action": "restart_acknowledged", "data": {"reason": reason, "delay": delay} }) - + # 设置全局重启标志 import unilabos.app.main as main_module main_module._restart_requested = True main_module._restart_reason = reason - + # 延迟后执行清理 await asyncio.sleep(delay) - + # 在新线程中执行清理,避免阻塞当前事件循环 def do_cleanup(): import time @@ -937,7 +937,7 @@ def do_cleanup(): logger.error("[MessageProcessor] Cleanup failed") except Exception as e: logger.error(f"[MessageProcessor] Error during cleanup: {e}") - + cleanup_thread = threading.Thread(target=do_cleanup, name="RestartCleanupThread", daemon=True) cleanup_thread.start() logger.info(f"[MessageProcessor] Restart cleanup scheduled") @@ -1375,7 +1375,7 @@ def publish_host_ready(self) -> None: # 收集设备信息 devices = [] machine_name = BasicConfig.machine_name - + try: host_node = HostNode.get_instance(0) if host_node: @@ -1383,7 +1383,7 @@ def publish_host_ready(self) -> None: for device_id, namespace in host_node.devices_names.items(): device_key = f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}" is_online = device_key in host_node._online_devices - + # 获取设备的动作信息 actions = {} for action_id, client in host_node._action_clients.items(): @@ -1394,7 +1394,7 @@ def publish_host_ready(self) -> None: "action_path": action_id, "action_type": str(type(client).__name__), } - + devices.append({ "device_id": device_id, "namespace": namespace, @@ -1403,7 +1403,7 @@ def publish_host_ready(self) -> None: "machine_name": host_node.device_machine_names.get(device_id, machine_name), "actions": actions, }) - + logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready") except Exception as e: logger.warning(f"[WebSocketClient] Error collecting device info: {e}") diff --git a/unilabos/device_comms/opcua_client/client.py b/unilabos/device_comms/opcua_client/client.py index b45cded2..27a401ff 100644 --- a/unilabos/device_comms/opcua_client/client.py +++ b/unilabos/device_comms/opcua_client/client.py @@ -141,7 +141,7 @@ def _connect(self) -> None: try: self.client.connect() logger.info('client connected!') - + # 连接后开始查找节点 if self._variables_to_find: self._find_nodes() @@ -150,36 +150,36 @@ def _connect(self) -> None: raise else: raise ValueError('client is not initialized') - + def _find_nodes(self) -> None: """查找服务器中的节点""" if not self.client: raise ValueError('client is not connected') - + logger.info(f'开始查找 {len(self._variables_to_find)} 个节点...') try: # 获取根节点 root = self.client.get_root_node() objects = root.get_child(["0:Objects"]) - + # 记录查找前的状态 before_count = len(self._node_registry) - + # 查找节点 self._find_nodes_recursive(objects) - + # 记录查找后的状态 after_count = len(self._node_registry) newly_found = after_count - before_count - + logger.info(f"本次查找新增 {newly_found} 个节点,当前共 {after_count} 个") - + # 检查是否所有节点都已找到 not_found = [] for var_name, var_info in self._variables_to_find.items(): if var_name not in self._node_registry: not_found.append(var_name) - + if not_found: logger.warning(f"⚠ 以下 {len(not_found)} 个节点未找到: {', '.join(not_found[:10])}{'...' if len(not_found) > 10 else ''}") logger.warning(f"提示:请检查这些节点名称是否与服务器的 BrowseName 完全匹配(包括大小写、空格等)") @@ -188,7 +188,7 @@ def _find_nodes(self) -> None: logger.info(f"尝试在服务器中查找第一个未找到的节点 '{not_found[0]}' 的相似节点...") else: logger.info(f"✓ 所有 {len(self._variables_to_find)} 个节点均已找到并注册") - + except Exception as e: logger.error(f"查找节点失败: {e}") traceback.print_exc() @@ -199,14 +199,14 @@ def _find_nodes_recursive(self, node) -> None: # 获取当前节点的浏览名称 browse_name = node.get_browse_name() node_name = browse_name.Name - + # 检查是否是我们要找的变量 if node_name in self._variables_to_find and node_name not in self._node_registry: var_info = self._variables_to_find[node_name] node_type = var_info.get("node_type") data_type = var_info.get("data_type") node_id_str = str(node.nodeid) - + # 根据节点类型创建相应的对象 if node_type == NodeType.VARIABLE: self._node_registry[node_name] = Variable(self.client, node_name, node_id_str, data_type) @@ -219,11 +219,11 @@ def _find_nodes_recursive(self, node) -> None: parent_node_id = str(parent_node.nodeid) self._node_registry[node_name] = Method(self.client, node_name, node_id_str, parent_node_id, data_type) logger.info(f"✓ 找到方法节点: '{node_name}', NodeId: {node_id_str}, ParentId: {parent_node_id}") - + # 递归处理子节点 for child in node.get_children(): self._find_nodes_recursive(child) - + except Exception as e: # 忽略处理单个节点时的错误,继续处理其他节点 pass @@ -238,50 +238,50 @@ def load_csv(cls, file_path: str) -> List[OpcUaNode]: df = pd.read_csv(file_path) df = df.drop_duplicates(subset='Name', keep='first') # 重复的数据应该报错 nodes = [] - + # 检查是否包含英文名称列和节点语言列 has_english_name = 'EnglishName' in df.columns has_node_language = 'NodeLanguage' in df.columns - + # 如果存在英文名称列,创建名称映射字典 name_mapping = {} reverse_mapping = {} - + for _, row in df.iterrows(): name = row.get('Name') node_type_str = row.get('NodeType') data_type_str = row.get('DataType') - + # 获取英文名称和节点语言(如果有) english_name = row.get('EnglishName') if has_english_name else None node_language = row.get('NodeLanguage') if has_node_language else 'English' # 默认为英文 - + # 如果有英文名称,添加到映射字典 if english_name and not pd.isna(english_name) and node_language == 'Chinese': name_mapping[english_name] = name reverse_mapping[name] = english_name - + if not name or not node_type_str: logger.warning(f"跳过无效行: 名称或节点类型缺失") continue - + # 只支持VARIABLE和METHOD两种类型 if node_type_str not in ['VARIABLE', 'METHOD']: logger.warning(f"不支持的节点类型: {node_type_str},仅支持VARIABLE和METHOD") continue - + try: node_type = NodeType[node_type_str] except KeyError: logger.warning(f"无效的节点类型: {node_type_str}") continue - + # 对于VARIABLE节点,必须指定数据类型 if node_type == NodeType.VARIABLE: if not data_type_str or pd.isna(data_type_str): logger.warning(f"变量节点 {name} 必须指定数据类型") continue - + try: data_type = DataType[data_type_str] except KeyError: @@ -295,14 +295,14 @@ def load_csv(cls, file_path: str) -> List[OpcUaNode]: data_type = DataType[data_type_str] except KeyError: logger.warning(f"无效的数据类型: {data_type_str},将使用默认值") - + # 创建节点对象,节点ID留空,将通过自动查找功能获取 nodes.append(OpcUaNode( name=name, node_type=node_type, data_type=data_type )) - + # 返回节点列表和名称映射字典 return nodes, name_mapping, reverse_mapping @@ -328,7 +328,7 @@ def use_node(self, name: str) -> OpcUaNodeBase: logger.info(f"重新查找成功: '{chinese_name}', NodeId: {node.node_id}") return node raise ValueError(f'节点 {chinese_name} (英文名: {name}) 未注册或未找到') - + # 直接使用原始名称查找 if name not in self._node_registry: if name in self._variables_to_find: @@ -368,14 +368,14 @@ def register_node_list(self, node_list: List[OpcUaNode]) -> "BaseClient": for node in node_list: if node is None: continue - + if node.name in self._node_registry: logger.debug(f'节点 "{node.name}" 已存在于注册表') exist = self._node_registry[node.name] if exist.type != node.node_type: raise ValueError(f'节点 {node.name} 类型 {node.node_type} 与已存在的类型 {exist.type} 不一致') continue - + # 将节点添加到待查找列表 self._variables_to_find[node.name] = { "node_type": node.node_type, @@ -385,11 +385,11 @@ def register_node_list(self, node_list: List[OpcUaNode]) -> "BaseClient": logger.debug(f'添加节点 "{node.name}" ({node.node_type}) 到待查找列表') logger.info(f'节点注册完成:新增 {new_nodes_count} 个待查找节点,总计 {len(self._variables_to_find)} 个') - + # 如果客户端已连接,立即开始查找 if self.client: self._find_nodes() - + return self def run_opcua_workflow(self, workflow: OpcUaWorkflow) -> None: @@ -480,7 +480,7 @@ def run_opcua_workflow_model(self, workflow: OpcUaWorkflowModel) -> bool: def create_node_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, **kwargs) -> Callable[[Callable[[str], OpcUaNodeBase]], bool]: def execute_node_function(use_node: Callable[[str], OpcUaNodeBase]) -> Union[bool, Tuple[Any, bool]]: target_node = use_node(node_name) - + # 检查是否有对应的参数值可用 current_value = value if hasattr(self, '_workflow_params') and func_name in self._workflow_params: @@ -488,19 +488,21 @@ def execute_node_function(use_node: Callable[[str], OpcUaNodeBase]) -> Union[boo print(f"使用参数值 {func_name} = {current_value}") else: print(f"执行 {node_name}, {type(target_node).__name__}, {target_node.node_id}, {mode}, {current_value}") - + if mode == 'read': result_str = self.read_node(node_name) - + try: # 将字符串转换为字典 result_str = result_str.replace("'", '"') # 替换单引号为双引号以便JSON解析 result_dict = json.loads(result_str) - + # 从字典获取值和错误标志 val = result_dict.get("value") err = result_dict.get("error") - + + print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}), 错误 = {err}") + print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}, 错误 = {err}") return val, err except Exception as e: @@ -510,7 +512,7 @@ def execute_node_function(use_node: Callable[[str], OpcUaNodeBase]) -> Union[boo # 构造完整的JSON输入,包含node_name和value input_json = json.dumps({"node_name": node_name, "value": current_value}) result_str = self.write_node(input_json) - + try: # 解析返回的字符串为字典 result_str = result_str.replace("'", '"') # 替换单引号为双引号以便JSON解析 @@ -527,19 +529,19 @@ def execute_node_function(use_node: Callable[[str], OpcUaNodeBase]) -> Union[boo print(f"调用方法 {node_name} 参数 = {args}, 返回值 = {result}") return result return False - + if func_name is None: func_name = f"{node_name}_{mode}_{str(value)}" - + print(f"创建 node function: {mode}, {func_name}") self.function_name[func_name] = execute_node_function - + return execute_node_function - + def create_init_function(self, func_name: str = None, write_nodes: Union[Dict[str, Any], List[str]] = None): """ 创建初始化函数 - + 参数: func_name: 函数名称 write_nodes: 写节点配置,可以是节点名列表[节点1,节点2]或节点值映射{节点1:值1,节点2:值2} @@ -599,25 +601,25 @@ def execute_init_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: except Exception as e: print(f"初始化函数: 解析写入结果失败: {e}, 原始结果: {result_str}") return True - + if func_name is None: func_name = f"init_function_{str(time.time())}" - + print(f"创建初始化函数: {func_name}") self.function_name[func_name] = execute_init_function return execute_init_function - + def create_stop_function(self, func_name: str = None, write_nodes: Union[Dict[str, Any], List[str]] = None): """ 创建停止函数 - + 参数: func_name: 函数名称 write_nodes: 写节点配置,可以是节点名列表[节点1,节点2]或节点值映射{节点1:值1,节点2:值2} """ if write_nodes is None: raise ValueError("必须提供write_nodes参数") - + def execute_stop_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: if isinstance(write_nodes, list): # 处理节点列表,默认值都是False @@ -647,25 +649,25 @@ def execute_stop_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: except Exception as e: print(f"停止函数: 解析写入结果失败: {e}, 原始结果: {result_str}") return True - + if func_name is None: func_name = f"stop_function_{str(time.time())}" - + print(f"创建停止函数: {func_name}") self.function_name[func_name] = execute_stop_function return execute_stop_function - + def create_cleanup_function(self, func_name: str = None, write_nodes: Union[Dict[str, Any], List[str]] = None): """ 创建清理函数 - + 参数: func_name: 函数名称 write_nodes: 写节点配置,可以是节点名列表[节点1,节点2]或节点值映射{节点1:值1,节点2:值2} """ if write_nodes is None: raise ValueError("必须提供write_nodes参数") - + def execute_cleanup_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: if isinstance(write_nodes, list): # 处理节点列表,默认值都是False @@ -695,10 +697,10 @@ def execute_cleanup_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: except Exception as e: print(f"清理函数: 解析写入结果失败: {e}, 原始结果: {result_str}") return True - + if func_name is None: func_name = f"cleanup_function_{str(time.time())}" - + print(f"创建清理函数: {func_name}") self.function_name[func_name] = execute_cleanup_function return execute_cleanup_function @@ -706,7 +708,7 @@ def execute_cleanup_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: def create_start_function(self, func_name: str, stop_condition_expression: str = "True", write_nodes: Union[Dict[str, Any], List[str]] = None, condition_nodes: Union[Dict[str, str], List[str]] = None): """ 创建开始函数 - + 参数: func_name: 函数名称 stop_condition_expression: 停止条件表达式,可直接引用节点名称 @@ -756,16 +758,16 @@ def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: print(f"直接写入 {node_name} = {current_value}, 结果: {success}") except Exception as e: print(f"解析直接写入结果失败: {e}, 原始结果: {result_str}") - + # 如果没有条件节点,立即返回 if not condition_nodes: return True - + # 处理条件检查和等待 while True: next_loop = False condition_source = {} - + # 直接读取条件节点 if isinstance(condition_nodes, list): # 处理节点列表 @@ -779,11 +781,11 @@ def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: read_res = result_dict.get("value") read_err = result_dict.get("error", False) print(f"直接读取 {node_name} 返回值 = {read_res}, 错误 = {read_err}") - + if read_err: next_loop = True break - + # 将节点值存入条件源字典,使用节点名称作为键 condition_source[node_name] = read_res # 为了向后兼容,也保留read_i格式 @@ -804,11 +806,11 @@ def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: read_res = result_dict.get("value") read_err = result_dict.get("error", False) print(f"直接读取 {node_name} 返回值 = {read_res}, 错误 = {read_err}") - + if read_err: next_loop = True break - + # 将节点值存入条件源字典 condition_source[node_name] = read_res # 也保存使用函数名作为键 @@ -817,13 +819,13 @@ def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: print(f"解析直接读取结果失败: {e}, 原始结果: {result_str}") next_loop = True break - + if not next_loop: if stop_condition_expression: # 添加调试信息 print(f"条件源数据: {condition_source}") condition_source["__RESULT"] = None - + # 确保安全地执行条件表达式 try: # 先尝试使用eval更安全的方式计算表达式 @@ -837,10 +839,10 @@ def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: except Exception as e2: print(f"使用exec执行表达式也失败: {e2}") condition_source["__RESULT"] = False - + res = condition_source["__RESULT"] print(f"取得计算结果: {res}, 条件表达式: {stop_condition_expression}") - + if res: print("满足停止条件,结束工作流") break @@ -849,21 +851,21 @@ def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: break else: time.sleep(0.3) - + return True - + self.function_name[func_name] = execute_start_function return execute_start_function create_action_from_json = None - + def create_action_from_json(self, data: Union[Dict, Any]) -> WorkflowAction: """ 从JSON配置创建工作流动作 - + 参数: data: 动作JSON数据 - + 返回: WorkflowAction对象 """ @@ -874,7 +876,7 @@ def create_action_from_json(self, data: Union[Dict, Any]) -> WorkflowAction: stop_function = None init_function = None cleanup_function = None - + # 提取start_function相关信息 if hasattr(data, "start_function") and data.start_function: start_function = data.start_function @@ -888,31 +890,31 @@ def create_action_from_json(self, data: Union[Dict, Any]) -> WorkflowAction: write_nodes = start_function["write_nodes"] if "condition_nodes" in start_function: condition_nodes = start_function["condition_nodes"] - + # 提取stop_function信息 if hasattr(data, "stop_function") and data.stop_function: stop_function = data.stop_function elif isinstance(data, dict) and data.get("stop_function"): stop_function = data.get("stop_function") - + # 提取init_function信息 if hasattr(data, "init_function") and data.init_function: init_function = data.init_function elif isinstance(data, dict) and data.get("init_function"): init_function = data.get("init_function") - + # 提取cleanup_function信息 if hasattr(data, "cleanup_function") and data.cleanup_function: cleanup_function = data.cleanup_function elif isinstance(data, dict) and data.get("cleanup_function"): cleanup_function = data.get("cleanup_function") - + # 创建工作流动作组件 init = None start = None stop = None cleanup = None - + # 处理init function if init_function: init_params = {"func_name": init_function.get("func_name")} @@ -921,9 +923,9 @@ def create_action_from_json(self, data: Union[Dict, Any]) -> WorkflowAction: else: # 如果没有write_nodes,创建一个空字典 init_params["write_nodes"] = {} - + init = self.create_init_function(**init_params) - + # 处理start function if start_function: start_params = { @@ -933,7 +935,7 @@ def create_action_from_json(self, data: Union[Dict, Any]) -> WorkflowAction: "condition_nodes": condition_nodes } start = self.create_start_function(**start_params) - + # 处理stop function if stop_function: stop_params = { @@ -941,7 +943,7 @@ def create_action_from_json(self, data: Union[Dict, Any]) -> WorkflowAction: "write_nodes": stop_function.get("write_nodes", {}) } stop = self.create_stop_function(**stop_params) - + # 处理cleanup function if cleanup_function: cleanup_params = { @@ -949,22 +951,22 @@ def create_action_from_json(self, data: Union[Dict, Any]) -> WorkflowAction: "write_nodes": cleanup_function.get("write_nodes", {}) } cleanup = self.create_cleanup_function(**cleanup_params) - + return WorkflowAction(init=init, start=start, stop=stop, cleanup=cleanup) - + workflow_name: Dict[str, OpcUaWorkflowModel] = {} def create_workflow_from_json(self, data: List[Dict]) -> None: """ 从JSON配置创建工作流程序 - + 参数: data: 工作流配置列表 """ for ind, flow_dict in enumerate(data): print(f"正在创建 workflow {ind}, {flow_dict['name']}") actions = [] - + for i in flow_dict["action"]: if isinstance(i, str): print(f"沿用已有 workflow 作为 action: {i}") @@ -973,14 +975,14 @@ def create_workflow_from_json(self, data: List[Dict]) -> None: print("创建 action") # 直接将字典转换为SimplifiedActionJson对象或直接使用字典 action = self.create_action_from_json(i) - + actions.append(action) - + # 获取参数 parameters = flow_dict.get("parameters", []) - + flow_instance = OpcUaWorkflowModel( - name=flow_dict["name"], + name=flow_dict["name"], actions=actions, parameters=parameters, description=flow_dict.get("description", "") @@ -1005,19 +1007,19 @@ def execute_procedure_from_json(self, data: Union[ExecuteProcedureJson, Dict]) - register_params = data.register_node_list_from_csv_path create_flow = data.create_flow execute_flow = data.execute_flow if hasattr(data, "execute_flow") else [] - + # 注册节点 if register_params: print(f"注册节点 csv: {register_params}") self.register_node_list_from_csv_path(**register_params) - + # 创建工作流 print("创建工作流") self.create_workflow_from_json(create_flow) - + # 注册工作流为实例方法 self.register_workflows_as_methods() - + # 如果存在execute_flow字段,则执行指定的工作流(向后兼容) if execute_flow: print("执行工作流") @@ -1029,12 +1031,12 @@ def register_workflows_as_methods(self) -> None: # 获取工作流的参数信息(如果存在) workflow_params = getattr(workflow, 'parameters', []) or [] workflow_desc = getattr(workflow, 'description', None) or f"执行工作流: {workflow_name}" - + # 创建执行工作流的方法 def create_workflow_method(wf_name=workflow_name, wf=workflow, params=workflow_params): def workflow_method(*args, **kwargs): logger.info(f"执行工作流: {wf_name}, 参数: {args}, {kwargs}") - + # 处理传入的参数 if params and (args or kwargs): # 将位置参数转换为关键字参数 @@ -1042,31 +1044,31 @@ def workflow_method(*args, **kwargs): for i, param_name in enumerate(params): if i < len(args): params_dict[param_name] = args[i] - + # 合并关键字参数 params_dict.update(kwargs) - + # 保存参数,供节点函数使用 self._workflow_params = params_dict else: self._workflow_params = {} - + # 执行工作流 result = self.run_opcua_workflow_model(wf) - + # 清理参数 self._workflow_params = {} - + return result - + # 设置方法的文档字符串 workflow_method.__doc__ = workflow_desc if params: param_doc = ", ".join(params) workflow_method.__doc__ += f"\n参数: {param_doc}" - + return workflow_method - + # 注册为实例方法 method = create_workflow_method() setattr(self, workflow_name, method) @@ -1082,7 +1084,7 @@ def read_node(self, node_name: str) -> Dict[str, Any]: try: node = self.use_node(node_name) value, error = node.read() - + # 创建结果字典 result = { "value": value, @@ -1090,7 +1092,7 @@ def read_node(self, node_name: str) -> Dict[str, Any]: "node_name": node_name, "timestamp": time.time() } - + # 返回JSON字符串 return json.dumps(result) except Exception as e: @@ -1104,7 +1106,7 @@ def read_node(self, node_name: str) -> Dict[str, Any]: "timestamp": time.time() } return json.dumps(result) - + def write_node(self, json_input: str) -> str: """ 写入节点值的便捷方法 @@ -1118,24 +1120,24 @@ def write_node(self, json_input: str) -> str: # 解析JSON格式的输入 if not isinstance(json_input, str): json_input = str(json_input) - + try: input_data = json.loads(json_input) if not isinstance(input_data, dict): return json.dumps({"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False}) - + # 从JSON中提取节点名称和值 node_name = input_data.get("node_name") value = input_data.get("value") - + if node_name is None: return json.dumps({"error": True, "error_message": "JSON中缺少node_name字段", "success": False}) except json.JSONDecodeError as e: return json.dumps({"error": True, "error_message": f"JSON解析错误: {str(e)}", "success": False}) - + node = self.use_node(node_name) error = node.write(value) - + # 创建结果字典 result = { "value": value, @@ -1144,7 +1146,7 @@ def write_node(self, json_input: str) -> str: "timestamp": time.time(), "success": not error } - + return json.dumps(result) except Exception as e: logger.error(f"写入节点失败: {e}") @@ -1155,7 +1157,7 @@ def write_node(self, json_input: str) -> str: "success": False } return json.dumps(result) - + def call_method(self, node_name: str, *args) -> Tuple[Any, bool]: """ 调用方法节点的便捷方法 @@ -1175,11 +1177,11 @@ def call_method(self, node_name: str, *args) -> Tuple[Any, bool]: class OpcUaClient(BaseClient): def __init__( - self, - url: str, + self, + url: str, deck: Optional[Union[post_process_deck, Dict[str, Any]]] = None, - config_path: str = None, - username: str = None, + config_path: str = None, + username: str = None, password: str = None, use_subscription: bool = True, cache_timeout: float = 5.0, @@ -1190,9 +1192,10 @@ def __init__( # 降低OPCUA库的日志级别 import logging logging.getLogger("opcua").setLevel(logging.WARNING) - + super().__init__() + # ===== 关键修改:参照 BioyondWorkstation 处理 deck ===== super().__init__() @@ -1215,15 +1218,15 @@ def __init__( if hasattr(self.deck, 'children'): warehouse_count = len(self.deck.children) logger.info(f"Deck 初始化完成,加载 {warehouse_count} 个资源") - - + + # OPC UA 客户端初始化 client = Client(url) - + if username and password: client.set_user(username) client.set_password(password) - + self._set_client(client) # 订阅相关属性 @@ -1231,30 +1234,30 @@ def __init__( self._subscription = None self._subscription_handles = {} self._subscription_interval = subscription_interval - + # 缓存相关属性 self._node_values = {} # 修改为支持时间戳的缓存结构 self._cache_timeout = cache_timeout - + # 连接状态监控 self._connection_check_interval = 30.0 # 连接检查间隔(秒) self._connection_monitor_running = False self._connection_monitor_thread = None - + # 添加线程锁,保护OPC UA客户端的并发访问 import threading self._client_lock = threading.RLock() - + # 连接到服务器 self._connect() - + # 如果提供了配置文件路径,则加载配置并注册工作流 if config_path: self.load_config(config_path) - + # 启动连接监控 self._start_connection_monitor() - + def _connect(self) -> None: """连接到OPC UA服务器""" @@ -1263,23 +1266,23 @@ def _connect(self) -> None: try: self.client.connect() logger.info('✓ 客户端已连接!') - + # 连接后开始查找节点 if self._variables_to_find: self._find_nodes() - + # 如果启用订阅模式,设置订阅 if self._use_subscription: self._setup_subscriptions() else: logger.info("订阅模式已禁用,将使用按需读取模式") - + except Exception as e: logger.error(f'客户端连接失败: {e}') raise else: raise ValueError('客户端未初始化') - + class SubscriptionHandler: """freeopcua订阅处理器:必须实现 datachange_notification 方法""" def __init__(self, outer): @@ -1300,22 +1303,22 @@ def _setup_subscriptions(self): """设置 OPC UA 订阅""" if not self.client or not self._use_subscription: return - + with self._client_lock: try: logger.info(f"开始设置订阅 (发布间隔: {self._subscription_interval}ms)...") - + # 创建订阅 handler = OpcUaClient.SubscriptionHandler(self) self._subscription = self.client.create_subscription( self._subscription_interval, handler ) - + # 为所有变量节点创建监控项 subscribed_count = 0 skipped_count = 0 - + for node_name, node in self._node_registry.items(): # 只为变量节点创建订阅 if node.type == NodeType.VARIABLE and node.node_id: @@ -1333,16 +1336,16 @@ def _setup_subscriptions(self): logger.warning(f"✗ 订阅节点 {node_name} 失败: {e}") else: skipped_count += 1 - + logger.info(f"订阅设置完成: 成功 {subscribed_count} 个, 跳过 {skipped_count} 个") - + except Exception as e: logger.error(f"设置订阅失败: {e}") traceback.print_exc() # 订阅失败时回退到按需读取模式 self._use_subscription = False logger.warning("订阅模式设置失败,已自动切换到按需读取模式") - + def _on_subscription_datachange(self, node, val, data): """订阅数据变化处理器(供内部 SubscriptionHandler 调用)""" try: @@ -1360,11 +1363,11 @@ def _on_subscription_datachange(self, node, val, data): break except Exception as e: logger.error(f"处理订阅数据失败: {e}") - + def get_node_value(self, name, use_cache=True, force_read=False): """ 获取节点值(智能缓存版本) - + 参数: name: 节点名称(支持中文名或英文名) use_cache: 是否使用缓存 @@ -1373,11 +1376,28 @@ def get_node_value(self, name, use_cache=True, force_read=False): # 处理名称映射 if name in self._name_mapping: chinese_name = self._name_mapping[name] + # 优先从缓存获取值 + if chinese_name in self._node_values: + return self._node_values[chinese_name] + # 缓存中没有则直接读取 + value, _ = self.use_node(chinese_name).read() + return value + # 如果提供的是中文名,直接使用 + elif name in self._node_registry: + # 优先从缓存获取值 + if name in self._node_values: + return self._node_values[name] + # 缓存中没有则直接读取 + value, _ = self.use_node(name).read() + return value + else: + raise ValueError(f"未找到名称为 '{name}' 的节点") + elif name in self._node_registry: chinese_name = name else: raise ValueError(f"未找到名称为 '{name}' 的节点") - + # 如果强制读取,直接从服务器读取 if force_read: with self._client_lock: @@ -1389,18 +1409,18 @@ def get_node_value(self, name, use_cache=True, force_read=False): 'source': 'forced_read' } return value - + # 检查缓存 if use_cache and chinese_name in self._node_values: cache_entry = self._node_values[chinese_name] cache_age = time.time() - cache_entry['timestamp'] - + # 如果是订阅模式,缓存永久有效(由订阅更新) # 如果是按需读取模式,检查缓存超时 if cache_entry.get('source') == 'subscription' or cache_age < self._cache_timeout: logger.debug(f"从缓存读取: {chinese_name} = {cache_entry['value']} (age: {cache_age:.2f}s, source: {cache_entry.get('source', 'unknown')})") return cache_entry['value'] - + # 缓存过期或不存在,从服务器读取 with self._client_lock: try: @@ -1419,7 +1439,7 @@ def get_node_value(self, name, use_cache=True, force_read=False): except Exception as e: logger.error(f"读取节点 {chinese_name} 出错: {e}") return None - + def set_node_value(self, name, value): """ 设置节点值 @@ -1432,12 +1452,28 @@ def set_node_value(self, name, value): chinese_name = name else: raise ValueError(f"未找到名称为 '{name}' 的节点") - + + # 写入值 + error = node.write(value) + if not error: + # 更新缓存 + if hasattr(node, 'name'): + self._node_values[node.name] = value + return True + return False + + def _refresh_worker(self): + """节点值刷新线程的工作函数""" + self._refresh_running = True + logger.info(f"节点值刷新线程已启动,刷新间隔: {self._refresh_interval}秒") + + while self._refresh_running: + with self._client_lock: try: node = self.use_node(chinese_name) error = node.write(value) - + if not error: # 写入成功,立即更新缓存 self._node_values[chinese_name] = { @@ -1453,7 +1489,7 @@ def set_node_value(self, name, value): except Exception as e: logger.error(f"写入节点 {chinese_name} 出错: {e}") return False - + def _check_connection(self) -> bool: """检查连接状态""" try: @@ -1466,22 +1502,33 @@ def _check_connection(self) -> bool: logger.warning(f"连接检查失败: {e}") return False return False - + def _connection_monitor_worker(self): """连接监控线程工作函数""" self._connection_monitor_running = True logger.info(f"连接监控线程已启动 (检查间隔: {self._connection_check_interval}秒)") - + reconnect_attempts = 0 max_reconnect_attempts = 5 - + while self._connection_monitor_running: try: + self.refresh_node_values() + except Exception as e: + logger.error(f"节点值刷新过程出错: {e}") + + # 等待下一次刷新 + time.sleep(self._refresh_interval) + + def start_node_refresh(self): + """启动节点值刷新线程""" + if self._refresh_thread is not None and self._refresh_thread.is_alive(): + logger.warning("节点值刷新线程已在运行") # 检查连接状态 if not self._check_connection(): logger.warning("检测到连接断开,尝试重新连接...") reconnect_attempts += 1 - + if reconnect_attempts <= max_reconnect_attempts: try: # 尝试重新连接 @@ -1491,14 +1538,14 @@ def _connection_monitor_worker(self): self.client.disconnect() except: pass - + self.client.connect() logger.info("✓ 重新连接成功") - + # 重新设置订阅 if self._use_subscription: self._setup_subscriptions() - + reconnect_attempts = 0 except Exception as e: logger.error(f"重新连接失败 (尝试 {reconnect_attempts}/{max_reconnect_attempts}): {e}") @@ -1509,34 +1556,44 @@ def _connection_monitor_worker(self): else: # 连接正常,重置重连计数 reconnect_attempts = 0 - + except Exception as e: logger.error(f"连接监控出错: {e}") - + # 等待下次检查 time.sleep(self._connection_check_interval) - + def _start_connection_monitor(self): """启动连接监控线程""" if self._connection_monitor_thread is not None and self._connection_monitor_thread.is_alive(): logger.warning("连接监控线程已在运行") return - + import threading + self._refresh_thread = threading.Thread(target=self._refresh_worker, daemon=True) + self._refresh_thread.start() + + def stop_node_refresh(self): + """停止节点值刷新线程""" + self._refresh_running = False + if self._refresh_thread and self._refresh_thread.is_alive(): + self._refresh_thread.join(timeout=2.0) + logger.info("节点值刷新线程已停止") + self._connection_monitor_thread = threading.Thread( - target=self._connection_monitor_worker, + target=self._connection_monitor_worker, daemon=True, name="OpcUaConnectionMonitor" ) self._connection_monitor_thread.start() - + def _stop_connection_monitor(self): """停止连接监控线程""" self._connection_monitor_running = False if self._connection_monitor_thread and self._connection_monitor_thread.is_alive(): self._connection_monitor_thread.join(timeout=2.0) logger.info("连接监控线程已停止") - + def read_node(self, node_name: str) -> str: """ 读取节点值的便捷方法(使用缓存) @@ -1545,11 +1602,11 @@ def read_node(self, node_name: str) -> str: try: # 使用get_node_value方法,自动处理缓存 value = self.get_node_value(node_name, use_cache=True) - + # 获取缓存信息 chinese_name = self._name_mapping.get(node_name, node_name) cache_info = self._node_values.get(chinese_name, {}) - + result = { "value": value, "error": False, @@ -1558,7 +1615,7 @@ def read_node(self, node_name: str) -> str: "cache_age": time.time() - cache_info.get('timestamp', time.time()), "source": cache_info.get('source', 'unknown') } - + return json.dumps(result) except Exception as e: logger.error(f"读取节点 {node_name} 失败: {e}") @@ -1582,21 +1639,21 @@ def get_cache_stats(self) -> Dict[str, Any]: 'cache_timeout': self._cache_timeout, 'using_subscription': self._use_subscription } - + for node_name, cache_entry in self._node_values.items(): source = cache_entry.get('source', 'unknown') cache_age = current_time - cache_entry['timestamp'] - + if source == 'subscription': stats['subscription_nodes'] += 1 elif source in ['on_demand_read', 'forced_read', 'write']: stats['on_demand_nodes'] += 1 - + if cache_age > self._cache_timeout: stats['expired_nodes'] += 1 - + return stats - + def print_cache_stats(self): """打印缓存统计信息""" stats = self.get_cache_stats() @@ -1610,37 +1667,41 @@ def print_cache_stats(self): print(f" - 已过期节点: {stats['expired_nodes']}") print(f"缓存超时时间: {stats['cache_timeout']}秒") print("="*80 + "\n") - + def load_config(self, config_path: str) -> None: """从JSON配置文件加载并注册工作流""" try: with open(config_path, 'r', encoding='utf-8') as f: config_data = json.load(f) - + # 处理节点注册 if "register_node_list_from_csv_path" in config_data: config_dir = os.path.dirname(os.path.abspath(config_path)) - + + # 处理CSV路径,如果是相对路径,则相对于配置文件所在目录 + if "path" in config_data["register_node_list_from_csv_path"]: csv_path = config_data["register_node_list_from_csv_path"]["path"] if not os.path.isabs(csv_path): csv_path = os.path.join(config_dir, csv_path) config_data["register_node_list_from_csv_path"]["path"] = csv_path - + + # 直接使用字典 + self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"]) - + if self.client and self._variables_to_find: logger.info("CSV加载完成,开始查找服务器节点...") self._find_nodes() - + # 处理工作流创建 if "create_flow" in config_data: self.create_workflow_from_json(config_data["create_flow"]) self.register_workflows_as_methods() - - # 将所有节点注册为属性 + + # 将所有节点注册为属性(只注册已找到的节点) self._register_nodes_as_attributes() - + # 打印统计信息 found_count = len(self._node_registry) total_count = len(self._variables_to_find) @@ -1648,23 +1709,114 @@ def load_config(self, config_path: str) -> None: logger.warning(f"节点查找完成:找到 {found_count}/{total_count} 个节点") else: logger.info(f"✓ 节点查找完成:所有 {found_count} 个节点均已找到") - + # 如果使用订阅模式,重新设置订阅(确保新节点被订阅) if self._use_subscription and found_count > 0: self._setup_subscriptions() - + logger.info(f"成功从 {config_path} 加载配置") except Exception as e: logger.error(f"加载配置文件 {config_path} 失败: {e}") traceback.print_exc() - + + def print_node_registry_status(self): + """打印节点注册状态,用于调试""" + print("\n" + "="*80) + print("节点注册状态诊断报告") + print("="*80) + print(f"\n待查找节点总数: {len(self._variables_to_find)}") + print(f"已找到节点总数: {len(self._node_registry)}") + print(f"未找到节点总数: {len(self._variables_to_find) - len(self._node_registry)}") + + # 显示已找到的节点(前10个) + if self._node_registry: + print(f"\n✓ 已找到的节点 (显示前10个):") + for i, (name, node) in enumerate(list(self._node_registry.items())[:10]): + eng_name = self._reverse_mapping.get(name, "") + eng_info = f" ({eng_name})" if eng_name else "" + print(f" {i+1}. '{name}'{eng_info}") + print(f" NodeId: {node.node_id}") + print(f" Type: {node.type}") + + # 显示未找到的节点 + not_found = [name for name in self._variables_to_find if name not in self._node_registry] + if not_found: + print(f"\n✗ 未找到的节点 (显示前20个):") + for i, name in enumerate(not_found[:20]): + eng_name = self._reverse_mapping.get(name, "") + eng_info = f" ({eng_name})" if eng_name else "" + node_info = self._variables_to_find[name] + print(f" {i+1}. '{name}'{eng_info} - {node_info['node_type']}") + + print("\n" + "="*80) + print("提示:") + print("1. 如果大量节点未找到,请检查CSV中的节点名称是否与服务器BrowseName完全匹配") + print("2. 可以使用 client.browse_server_nodes() 查看服务器的实际节点结构") + print("3. 节点名称区分大小写,且包括所有空格和特殊字符") + print("="*80 + "\n") + + def browse_server_nodes(self, max_depth=3, start_path=["0:Objects"]): + """浏览服务器节点树,用于调试和对比""" + if not self.client: + print("客户端未连接") + return + + print("\n" + "="*80) + print(f"服务器节点浏览 (最大深度: {max_depth})") + print("="*80 + "\n") + + try: + root = self.client.get_root_node() + start_node = root.get_child(start_path) + self._browse_node_recursive(start_node, depth=0, max_depth=max_depth) + except Exception as e: + print(f"浏览失败: {e}") + traceback.print_exc() + + def _browse_node_recursive(self, node, depth=0, max_depth=3): + """递归浏览节点""" + if depth > max_depth: + return + + try: + browse_name = node.get_browse_name() + node_class = node.get_node_class() + indent = " " * depth + + # 显示节点信息 + print(f"{indent}├─ {browse_name.Name}") + print(f"{indent}│ NodeId: {str(node.nodeid)}") + print(f"{indent}│ NodeClass: {node_class}") + + # 如果是变量,显示数据类型 + if node_class == NodeClass.Variable: + try: + data_type = node.get_data_type() + print(f"{indent}│ DataType: {data_type}") + except: + pass + + # 递归处理子节点(限制数量避免输出过多) + if depth < max_depth: + children = node.get_children() + for i, child in enumerate(children[:20]): # 每层最多显示20个子节点 + self._browse_node_recursive(child, depth + 1, max_depth) + if len(children) > 20: + print(f"{indent} ... ({len(children) - 20} more children)") + except Exception as e: + # 忽略单个节点的错误 + pass + def disconnect(self): + # 停止刷新线程 + self.stop_node_refresh() + """断开连接并清理资源""" logger.info("正在断开连接...") - + # 停止连接监控 self._stop_connection_monitor() - + # 删除订阅 if self._subscription: try: @@ -1673,7 +1825,7 @@ def disconnect(self): logger.info("订阅已删除") except Exception as e: logger.warning(f"删除订阅失败: {e}") - + # 断开客户端连接 if self.client: try: @@ -1682,22 +1834,22 @@ def disconnect(self): logger.info("✓ OPC UA 客户端已断开连接") except Exception as e: logger.error(f"断开连接失败: {e}") - + def _register_nodes_as_attributes(self): """将所有节点注册为实例属性""" for node_name, node in self._node_registry.items(): if not node.node_id or node.node_id == "": logger.warning(f"⚠ 节点 '{node_name}' 的 node_id 为空,跳过注册为属性") continue - + eng_name = self._reverse_mapping.get(node_name) attr_name = eng_name if eng_name else node_name.replace(' ', '_').replace('-', '_') - + def create_property_getter(node_key): def getter(self): return self.get_node_value(node_key, use_cache=True) return getter - + setattr(OpcUaClient, attr_name, property(create_property_getter(node_name))) logger.debug(f"已注册节点 '{node_name}' 为属性 '{attr_name}'") @@ -1705,14 +1857,14 @@ def post_init(self, ros_node): """ROS2 节点就绪后的初始化""" if not (hasattr(self, 'deck') and self.deck): return - + if not (hasattr(ros_node, 'resource_tracker') and ros_node.resource_tracker): logger.warning("resource_tracker 不存在,无法注册 deck") return - + # 1. 本地注册(必需) ros_node.resource_tracker.add_resource(self.deck) - + # 2. 上传云端 try: from unilabos.ros.nodes.base_device_node import ROS2DeviceNode @@ -1728,30 +1880,30 @@ def post_init(self, ros_node): if __name__ == '__main__': # 示例用法 - + # 使用配置文件创建客户端并自动注册工作流 import os current_dir = os.path.dirname(os.path.abspath(__file__)) config_path = os.path.join(current_dir, "opcua_huairou.json") - + # 创建OPC UA客户端并加载配置 try: client = OpcUaClient( url="opc.tcp://192.168.1.88:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址 config_path="D:\\Uni-Lab-OS\\unilabos\\device_comms\\opcua_client\\opcua_huairou.json" # 传入配置文件路径 ) - + # 列出所有已注册的工作流 print("\n已注册的工作流:") for workflow_name in client.workflow_name: print(f" - {workflow_name}") - + # 测试trigger_grab_action工作流 - 使用英文参数名 print("\n测试trigger_grab_action工作流 - 使用英文参数名:") client.trigger_grab_action(reaction_tank_number=2, raw_tank_number=2) # client.set_node_value("reaction_tank_number", 2) - + # 读取节点值 - 使用英文节点名 grab_complete = client.get_node_value("grab_complete") reaction_tank = client.get_node_value("reaction_tank_number") @@ -1761,19 +1913,19 @@ def post_init(self, ros_node): print(f" - 抓取完成状态: {grab_complete}") print(f" - 当前反应罐号码: {reaction_tank}") print(f" - 当前原料罐号码: {raw_tank}") - + # 测试节点值写入 - 使用英文节点名 print("\n测试节点值写入 (使用英文节点名):") success = client.set_node_value("atomization_fast_speed", 150.5) print(f" - 写入搅拌浆雾化快速 = 150.5, 结果: {success}") - + # 读取写入的值 atomization_speed = client.get_node_value("atomization_fast_speed") print(f" - 读取搅拌浆雾化快速: {atomization_speed}") - + # 断开连接 client.disconnect() - + except Exception as e: print(f"错误: {e}") traceback.print_exc() diff --git a/unilabos/device_comms/opcua_client/node/uniopcua.py b/unilabos/device_comms/opcua_client/node/uniopcua.py index d99a5fd7..a06d780c 100644 --- a/unilabos/device_comms/opcua_client/node/uniopcua.py +++ b/unilabos/device_comms/opcua_client/node/uniopcua.py @@ -43,7 +43,7 @@ def __init__(self, client: Client, name: str, node_id: str, typ: NodeType, data_ self._type = typ self._data_type = data_type self._node: Optional[Node] = None - + def _get_node(self) -> Node: if self._node is None: try: @@ -66,7 +66,7 @@ def _get_node(self) -> Node: # 直接以字符串形式处理 if isinstance(nid, str): nid = nid.strip() - + # 处理包含类名的格式,如 'StringNodeId(ns=4;s=...)' 或 'NumericNodeId(ns=2;i=...)' # 提取括号内的内容 match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid) @@ -116,16 +116,16 @@ def _get_node(self) -> Node: def read(self) -> Tuple[Any, bool]: """读取节点值,返回(值, 是否出错)""" pass - + @abstractmethod def write(self, value: Any) -> bool: """写入节点值,返回是否出错""" pass - + @property def type(self) -> NodeType: return self._type - + @property def node_id(self) -> str: return self._node_id @@ -210,15 +210,15 @@ def __init__(self, client: Client, name: str, node_id: str, parent_node_id: str, super().__init__(client, name, node_id, NodeType.METHOD, data_type) self._parent_node_id = parent_node_id self._parent_node = None - + def _get_parent_node(self) -> Node: if self._parent_node is None: try: # 处理父节点ID,使用与_get_node相同的解析逻辑 import re - + nid = self._parent_node_id - + # 如果已经是 NodeId 对象,直接使用 try: from opcua.ua import NodeId as UaNodeId @@ -227,16 +227,16 @@ def _get_parent_node(self) -> Node: return self._parent_node except Exception: pass - + # 字符串处理 if isinstance(nid, str): nid = nid.strip() - + # 处理包含类名的格式 match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid) if match_wrapped: nid = match_wrapped.group(2).strip() - + # 常见短格式 if re.match(r'^ns=\d+;[is]=', nid): self._parent_node = self._client.get_node(nid) @@ -271,7 +271,7 @@ def read(self) -> Tuple[Any, bool]: def write(self, value: Any) -> bool: """方法节点不支持写入操作""" return True - + def call(self, *args) -> Tuple[Any, bool]: """调用方法,返回(返回值, 是否出错)""" try: @@ -285,7 +285,7 @@ def call(self, *args) -> Tuple[Any, bool]: class Object(Base): def __init__(self, client: Client, name: str, node_id: str): super().__init__(client, name, node_id, NodeType.OBJECT, None) - + def read(self) -> Tuple[Any, bool]: """对象节点不支持直接读取操作""" return None, True @@ -293,7 +293,7 @@ def read(self) -> Tuple[Any, bool]: def write(self, value: Any) -> bool: """对象节点不支持直接写入操作""" return True - + def get_children(self) -> Tuple[List[Node], bool]: """获取子节点列表,返回(子节点列表, 是否出错)""" try: @@ -301,4 +301,4 @@ def get_children(self) -> Tuple[List[Node], bool]: return children, False except Exception as e: print(f"获取对象 {self._name} 的子节点失败: {e}") - return [], True \ No newline at end of file + return [], True diff --git a/unilabos/devices/workstation/README.md b/unilabos/devices/workstation/README.md index f96ed7f0..1c38f401 100644 --- a/unilabos/devices/workstation/README.md +++ b/unilabos/devices/workstation/README.md @@ -1,9 +1,5 @@ # 工作站抽象基类物料系统架构说明 -## 设计理念 - -基于用户需求"请你帮我系统思考一下,工作站抽象基类的物料系统基类该如何构建",我们最终确定了一个**PyLabRobot Deck为中心**的简化架构。 - ### 核心原则 1. **PyLabRobot为物料管理核心**:使用PyLabRobot的Deck系统作为物料管理的基础,利用其成熟的Resource体系 diff --git "a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20251229_\345\244\232\350\256\242\345\215\225\350\277\224\345\233\236\350\257\264\346\230\216.md" "b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20251229_\345\244\232\350\256\242\345\215\225\350\277\224\345\233\236\350\257\264\346\230\216.md" new file mode 100644 index 00000000..baecac95 --- /dev/null +++ "b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20251229_\345\244\232\350\256\242\345\215\225\350\277\224\345\233\236\350\257\264\346\230\216.md" @@ -0,0 +1,113 @@ +# Bioyond Cell 工作站 - 多订单返回示例 + +本文档说明了 `create_orders` 函数如何收集并返回所有订单的完成报文。 + +## 问题描述 + +之前的实现只会等待并返回第一个订单的完成报文,如果有多个订单(例如从 Excel 解析出 3 个订单),只能得到第一个订单的推送信息。 + +## 解决方案 + +修改后的 `create_orders` 函数现在会: + +1. **提取所有 orderCode**:从 LIMS 接口返回的 `data` 列表中提取所有订单编号 +2. **逐个等待完成**:遍历所有 orderCode,调用 `wait_for_order_finish` 等待每个订单完成 +3. **收集所有报文**:将每个订单的完成报文存入 `all_reports` 列表 +4. **统一返回**:返回包含所有订单报文的 JSON 格式数据 + +## 返回格式 + +```json +{ + "status": "all_completed", + "total_orders": 3, + "reports": [ + { + "token": "", + "request_time": "2025-12-24T15:32:09.2148671+08:00", + "data": { + "orderId": "3a1e614d-a082-c44a-60be-68647a35e6f1", + "orderCode": "BSO2025122400024", + "orderName": "DP20251224001", + "status": "30", + "workflowStatus": "completed", + "usedMaterials": [...] + } + }, + { + "token": "", + "request_time": "2025-12-24T15:32:09.9999039+08:00", + "data": { + "orderId": "3a1e614d-a0a2-f7a9-9360-610021c9479d", + "orderCode": "BSO2025122400025", + "orderName": "DP20251224002", + "status": "30", + "workflowStatus": "completed", + "usedMaterials": [...] + } + }, + { + "token": "", + "request_time": "2025-12-24T15:34:00.4139986+08:00", + "data": { + "orderId": "3a1e614d-a0cd-81ca-9f7f-2f4e93af01cd", + "orderCode": "BSO2025122400026", + "orderName": "DP20251224003", + "status": "30", + "workflowStatus": "completed", + "usedMaterials": [...] + } + } + ], + "original_response": {...} +} +``` + +## 使用示例 + +```python +# 调用 create_orders +result = workstation.create_orders("20251224.xlsx") + +# 访问返回数据 +print(f"总订单数: {result['total_orders']}") +print(f"状态: {result['status']}") + +# 遍历所有订单的报文 +for i, report in enumerate(result['reports'], 1): + order_data = report.get('data', {}) + print(f"\n订单 {i}:") + print(f" orderCode: {order_data.get('orderCode')}") + print(f" orderName: {order_data.get('orderName')}") + print(f" status: {order_data.get('status')}") + print(f" 使用物料数: {len(order_data.get('usedMaterials', []))}") +``` + +## 控制台输出示例 + +``` +[create_orders] 即将提交订单数量: 3 +[create_orders] 接口返回: {...} +[create_orders] 等待 3 个订单完成: ['BSO2025122400024', 'BSO2025122400025', 'BSO2025122400026'] +[create_orders] 正在等待第 1/3 个订单: BSO2025122400024 +[create_orders] ✓ 订单 BSO2025122400024 完成 +[create_orders] 正在等待第 2/3 个订单: BSO2025122400025 +[create_orders] ✓ 订单 BSO2025122400025 完成 +[create_orders] 正在等待第 3/3 个订单: BSO2025122400026 +[create_orders] ✓ 订单 BSO2025122400026 完成 +[create_orders] 所有订单已完成,共收集 3 个报文 +实验记录本========================create_orders======================== +返回报文数量: 3 +报文 1: orderCode=BSO2025122400024, status=30 +报文 2: orderCode=BSO2025122400025, status=30 +报文 3: orderCode=BSO2025122400026, status=30 +======================== +``` + +## 关键改进 + +1. ✅ **等待所有订单**:不再只等待第一个订单,而是遍历所有 orderCode +2. ✅ **收集完整报文**:每个订单的完整推送报文都被保存在 `reports` 数组中 +3. ✅ **详细日志**:清晰显示正在等待哪个订单,以及完成情况 +4. ✅ **错误处理**:即使某个订单失败,也会记录其状态信息 +5. ✅ **统一格式**:返回的 JSON 格式便于后续处理和分析 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 00000000..9088a16a 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/20260113_JSON\351\205\215\347\275\256\350\277\201\347\247\273\347\273\217\351\252\214.md" "b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260113_JSON\351\205\215\347\275\256\350\277\201\347\247\273\347\273\217\351\252\214.md" new file mode 100644 index 00000000..c48cd439 --- /dev/null +++ "b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260113_JSON\351\205\215\347\275\256\350\277\201\347\247\273\347\273\217\351\252\214.md" @@ -0,0 +1,204 @@ +# BioyondCellWorkstation JSON 配置迁移经验总结 + +**日期**: 2026-01-13 +**目的**: 从 `config.py` 迁移到 JSON 配置文件 + +--- + +## 问题背景 + +原系统通过 `config.py` 管理配置,导致: +1. HTTP 服务重复启动(父类 `BioyondWorkstation` 和子类都启动) +2. 配置分散在代码中,不便于管理 +3. 无法通过 JSON 统一配置所有参数 + +--- + +## 解决方案:嵌套配置结构 + +### JSON 结构设计 + +**正确示例** (嵌套在 `config` 中): +```json +{ + "nodes": [{ + "id": "bioyond_cell_workstation", + "config": { + "deck": {...}, + "protocol_type": [], + "bioyond_config": { + "api_host": "http://172.16.11.219:44388", + "api_key": "8A819E5C", + "timeout": 30, + "HTTP_host": "172.16.11.206", + "HTTP_port": 8080, + "debug_mode": false, + "material_type_mappings": {...}, + "warehouse_mapping": {...}, + "solid_liquid_mappings": {...} + } + }, + "data": {} + }] +} +``` + +**关键点**: +- ✅ `bioyond_config` 放在 `config` 中(会传递到 `__init__`) +- ❌ **不要**放在 `data` 中(`data` 是运行时状态,不会传递) + +--- + +## Python 代码适配 + +### 1. 修改 `BioyondCellWorkstation.__init__` 签名 + +**文件**: `bioyond_cell_workstation.py` + +```python +def __init__(self, bioyond_config: dict = None, deck=None, protocol_type=None, **kwargs): + """ + Args: + bioyond_config: 从 JSON 加载的配置字典 + deck: Deck 配置 + protocol_type: 协议类型 + """ + # 验证配置 + if bioyond_config is None: + raise ValueError("需要 bioyond_config 参数") + + # 保存配置 + self.bioyond_config = bioyond_config + + # 设置 HTTP 服务去重标志 + self.bioyond_config["_disable_auto_http_service"] = True + + # 调用父类 + super().__init__(bioyond_config=self.bioyond_config, deck=deck, **kwargs) +``` + +### 2. 替换全局变量引用 + +**修改前**(使用全局变量): +```python +from config import MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING + +def create_sample(self, board_type, ...): + carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1] + location_id = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"][location_code] +``` + +**修改后**(从配置读取): +```python +def create_sample(self, board_type, ...): + carrier_type_id = self.bioyond_config['material_type_mappings'][board_type][1] + location_id = self.bioyond_config['warehouse_mapping'][warehouse_name]["site_uuids"][location_code] +``` + +### 3. 修复父类配置访问 + +在 `station.py` 中安全访问配置默认值: + +```python +# 修改前(会 KeyError) +self._http_service_config = { + "host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG["http_service_host"]) +} + +# 修改后(安全访问) +self._http_service_config = { + "host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG.get("http_service_host", "")) +} +``` + +--- + +## 常见陷阱 + +### ❌ 错误1:将配置放在 `data` 字段 +```json +"config": {"deck": {...}}, +"data": {"bioyond_config": {...}} // ❌ 不会传递到 __init__ +``` + +### ❌ 错误2:扁平化配置(已废弃方案) +虽然扁平化也能工作,但不推荐: +```json +"config": { + "deck": {...}, + "api_host": "...", // ❌ 不够清晰 + "api_key": "...", + "HTTP_host": "..." +} +``` + +### ❌ 错误3:忘记替换全局变量引用 +代码中直接使用 `MATERIAL_TYPE_MAPPINGS` 等全局变量会导致 `NameError`。 + +--- + +## 云端同步注意事项 + +使用 `--upload_registry` 时,云端配置可能覆盖本地配置: +- 首次上传时确保 JSON 完整 +- 或使用新的 `ak/sk` 避免旧配置干扰 +- 调试时可暂时移除 `--upload_registry` 参数 + +--- + +## 验证清单 + +启动成功后应看到: +``` +✅ 从 JSON 配置加载 bioyond_config 成功 + API Host: http://... + HTTP Service: ... +✅ BioyondCellWorkstation 初始化完成 +Loaded ResourceTreeSet with ... nodes +``` + +运行时不应出现: +- ❌ `NameError: name 'MATERIAL_TYPE_MAPPINGS' is not defined` +- ❌ `KeyError: 'http_service_host'` +- ❌ `bioyond_config 缺少必需参数` + +--- + +## 调试经验 + +1. **添加调试日志**查看参数传递链路: + - `graphio.py`: JSON 加载后的 config 内容 + - `initialize_device.py`: `device_config.res_content.config` 的键 + - `bioyond_cell_workstation.py`: `__init__` 接收到的参数 + +2. **config vs data 区别**: + - `config`: 初始化参数,传递给 `__init__` + - `data`: 运行时状态,不传递给 `__init__` + +3. **参数名必须匹配**: + - JSON 中的键名必须与 `__init__` 参数名完全一致 + +4. **调试代码清理**:完成后记得删除调试日志(🔍 DEBUG 标记) + +--- + +## 修改文件清单 + +| 文件 | 修改内容 | +|------|----------| +| `yibin_electrolyte_config.json` | 创建嵌套 `config.bioyond_config` 结构 | +| `bioyond_cell_workstation.py` | 修改 `__init__` 接收 `bioyond_config`,替换所有全局变量引用 | +| `station.py` | 安全访问 `HTTP_SERVICE_CONFIG` 默认值 | + +--- + +## 参考代码位置 + +- JSON 配置示例: `yibin_electrolyte_config.json` L12-L353 +- `__init__` 实现: `bioyond_cell_workstation.py` L39-L94 +- 全局变量替换示例: `bioyond_cell_workstation.py` L2005, L1863, L1966 +- HTTP 服务配置: `station.py` L629-L634 + +--- + +**总结**: 使用嵌套结构将所有配置放在 `config.bioyond_config` 中,修改 `__init__` 直接接收该参数,并替换所有全局变量引用为 `self.bioyond_config` 访问。 diff --git "a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260113_\351\205\215\347\275\256\350\277\201\347\247\273\344\277\256\346\224\271\346\200\273\347\273\223.md" "b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260113_\351\205\215\347\275\256\350\277\201\347\247\273\344\277\256\346\224\271\346\200\273\347\273\223.md" new file mode 100644 index 00000000..1113549e --- /dev/null +++ "b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260113_\351\205\215\347\275\256\350\277\201\347\247\273\344\277\256\346\224\271\346\200\273\347\273\223.md" @@ -0,0 +1,312 @@ +# BioyondCell 配置迁移修改总结 + +**日期**: 2026-01-13 +**目标**: 从 `config.py` 完全迁移到 JSON 配置,消除所有全局变量依赖 + +--- + +## 📋 修改概览 + +本次修改完成了 BioyondCell 模块从 Python 配置文件到 JSON 配置的完整迁移,并清理了所有对 `config.py` 全局变量的依赖。 + +### 核心成果 + +- ✅ 完全移除对 `config.py` 的导入依赖 +- ✅ 使用嵌套 JSON 结构 `config.bioyond_config` +- ✅ 修复 7 处 `bioyond_cell_workstation.py` 中的全局变量引用 +- ✅ 修复 3 处其他文件中的全局变量引用 +- ✅ HTTP 服务去重机制完善 +- ✅ 系统成功启动并正常运行 + +--- + +## 🔧 修改文件清单 + +### 1. JSON 配置文件 + +**文件**: `yibin_electrolyte_config.json` + +**修改**: +- 采用嵌套结构将所有配置放在 `config.bioyond_config` 中 +- 包含:`api_host`, `api_key`, `HTTP_host`, `HTTP_port`, `material_type_mappings`, `warehouse_mapping`, `solid_liquid_mappings` 等 + +**示例结构**: +```json +{ + "nodes": [{ + "id": "bioyond_cell_workstation", + "config": { + "deck": {...}, + "protocol_type": [], + "bioyond_config": { + "api_host": "http://172.16.11.219:44388", + "api_key": "8A819E5C", + "HTTP_host": "172.16.11.206", + "HTTP_port": 8080, + "material_type_mappings": {...}, + "warehouse_mapping": {...}, + "solid_liquid_mappings": {...} + } + } + }] +} +``` + +--- + +### 2. bioyond_cell_workstation.py + +**位置**: `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py` + +#### 修改 A: `__init__` 方法签名 (L39-99) + +**修改前**: +```python +def __init__(self, deck=None, protocol_type=None, **kwargs): + # 从 kwargs 收集配置字段 + self.bioyond_config = {} + for field in bioyond_field_names: + if field in kwargs: + self.bioyond_config[field] = kwargs.pop(field) +``` + +**修改后**: +```python +def __init__(self, bioyond_config: dict = None, deck=None, protocol_type=None, **kwargs): + """直接接收 bioyond_config 参数""" + if bioyond_config is None: + raise ValueError("需要 bioyond_config 参数") + + self.bioyond_config = bioyond_config + + # 设置 HTTP 服务去重标志 + self.bioyond_config["_disable_auto_http_service"] = True + + super().__init__(bioyond_config=self.bioyond_config, deck=deck, **kwargs) +``` + +#### 修改 B: 替换全局变量引用 (7 处) + +| 位置 | 原代码 | 修改后 | +|------|--------|--------| +| L2005 | `MATERIAL_TYPE_MAPPINGS[board_type][1]` | `self.bioyond_config['material_type_mappings'][board_type][1]` | +| L2006 | `MATERIAL_TYPE_MAPPINGS[bottle_type][1]` | `self.bioyond_config['material_type_mappings'][bottle_type][1]` | +| L2009 | `WAREHOUSE_MAPPING` | `self.bioyond_config['warehouse_mapping']` | +| L2013 | `WAREHOUSE_MAPPING[warehouse_name]` | `self.bioyond_config['warehouse_mapping'][warehouse_name]` | +| L2017 | `WAREHOUSE_MAPPING[warehouse_name]["site_uuids"]` | `self.bioyond_config['warehouse_mapping'][warehouse_name]["site_uuids"]` | +| L1863 | `SOLID_LIQUID_MAPPINGS.get(material_name)` | `self.bioyond_config.get('solid_liquid_mappings', {}).get(material_name)` | +| L1966, L1976 | `MATERIAL_TYPE_MAPPINGS.items()` | `self.bioyond_config['material_type_mappings'].items()` | + +--- + +### 3. station.py + +**位置**: `unilabos/devices/workstation/bioyond_studio/station.py` + +#### 修改 A: 删除 config 导入 (L26-28) + +**修改前**: +```python +from unilabos.devices.workstation.bioyond_studio.config import ( + API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, HTTP_SERVICE_CONFIG +) +``` + +**修改后**: +```python +# 已删除此导入 +``` + +#### 修改 B: `_create_communication_module` 方法 (L691-702) + +**修改前**: +```python +def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: + default_config = { + **API_CONFIG, + "workflow_mappings": WORKFLOW_MAPPINGS, + "material_type_mappings": MATERIAL_TYPE_MAPPINGS, + "warehouse_mapping": WAREHOUSE_MAPPING + } + if config: + self.bioyond_config = {**default_config, **config} + else: + self.bioyond_config = default_config +``` + +**修改后**: +```python +def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: + """创建Bioyond通信模块""" + # 使用传入的 config 参数(来自 bioyond_config) + # 不再依赖全局变量 API_CONFIG 等 + if config: + self.bioyond_config = config + else: + # 如果没有传入配置,创建空配置(用于测试或兼容性) + self.bioyond_config = {} + + self.hardware_interface = BioyondV1RPC(self.bioyond_config) +``` + +#### 修改 C: HTTP 服务配置 (L627-632) + +**修改前**: +```python +self._http_service_config = { + "host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG.get("http_service_host", "")), + "port": bioyond_config.get("http_service_port", HTTP_SERVICE_CONFIG.get("http_service_port", 0)) +} +``` + +**修改后**: +```python +self._http_service_config = { + "host": bioyond_config.get("http_service_host", bioyond_config.get("HTTP_host", "")), + "port": bioyond_config.get("http_service_port", bioyond_config.get("HTTP_port", 0)) +} +``` + +--- + +### 4. bioyond_rpc.py + +**位置**: `unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py` + +#### 修改 A: 删除 config 导入 (L12) + +**修改前**: +```python +from unilabos.devices.workstation.bioyond_studio.config import LOCATION_MAPPING +``` + +**修改后**: +```python +# 已删除此导入 +``` + +#### 修改 B: `material_outbound` 方法 (L278-280) + +**修改前**: +```python +def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict: + """指定库位出库物料(通过库位名称)""" + location_id = LOCATION_MAPPING.get(location_name, location_name) +``` + +**修改后**: +```python +def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict: + """指定库位出库物料(通过库位名称)""" + # location_name 参数实际上应该直接是 location_id (UUID) + location_id = location_name +``` + +**说明**: `LOCATION_MAPPING` 在 `config-0113.py` 中本来就是空字典 `{}`,所以直接使用 `location_name` 逻辑等价。 + +--- + +## 🎯 关键设计决策 + +### 1. 嵌套 vs 扁平配置 + +**选择**: 嵌套结构 `config.bioyond_config` + +**理由**: +- ✅ 语义清晰,配置分组明确 +- ✅ 参数传递直观,直接对应 `__init__` 参数 +- ✅ 易于维护,不需要硬编码字段列表 +- ✅ 符合 UniLab 设计模式 + +### 2. HTTP 服务去重 + +**实现**: 子类设置 `_disable_auto_http_service` 标志 + +```python +# bioyond_cell_workstation.py +self.bioyond_config["_disable_auto_http_service"] = True + +# station.py (post_init) +if self.bioyond_config.get("_disable_auto_http_service"): + logger.info("子类已自行管理HTTP服务,跳过自动启动") + return +``` + +### 3. 全局变量替换策略 + +**原则**: 所有配置从 `self.bioyond_config` 获取 + +**模式**: +```python +# 修改前 +from config import MATERIAL_TYPE_MAPPINGS +carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1] + +# 修改后 +carrier_type_id = self.bioyond_config['material_type_mappings'][board_type][1] +``` + +--- + +## ✅ 验证结果 + +### 启动成功日志 +``` +✅ 从 JSON 配置加载 bioyond_config 成功 + API Host: http://172.16.11.219:44388 + HTTP Service: 172.16.11.206:8080 +🔧 已设置 _disable_auto_http_service 标志,防止 HTTP 服务重复启动 +✅ BioyondCellWorkstation 初始化完成 +Loaded ResourceTreeSet with 1 trees, 1785 total nodes +``` + +### 功能验证 +- ✅ 订单创建 (`create_orders_v2`) +- ✅ 质量比计算 +- ✅ 物料转移 (`transfer_3_to_2_to_1`) +- ✅ HTTP 报送接收 (step_finish, sample_finish, order_finish) +- ✅ 等待机制 (`wait_for_order_finish`) +- ✅ 仓库 UUID 映射 +- ✅ 物料类型映射 + +--- + +## 📚 相关文档 + +- **配置迁移经验**: `2026-01-13_JSON配置迁移经验.md` +- **任务清单**: `C:\Users\AndyXie\.gemini\antigravity\brain\...\task.md` +- **实施计划**: `C:\Users\AndyXie\.gemini\antigravity\brain\...\implementation_plan.md` + +--- + +## ⚠️ 注意事项 + +### 其他工作站模块 + +以下文件仍在使用 `config.py` 全局变量(未包含在本次修改中): +- `reaction_station.py` - 使用 `API_CONFIG` +- `experiment.py` - 使用 `API_CONFIG`, `WORKFLOW_MAPPINGS`, `MATERIAL_TYPE_MAPPINGS` +- `dispensing_station.py` - 使用 `API_CONFIG`, `WAREHOUSE_MAPPING` +- `station.py` L176, L177, L529, L530 - 动态导入 `WAREHOUSE_MAPPING` + +**建议**: 后续可以统一迁移这些模块到 JSON 配置。 + +### config.py 文件 + +`config.py` 文件已恢复但**不再被 bioyond_cell 使用**。可以: +- 保留作为其他模块的参考 +- 或者完全删除(如果其他模块也迁移完成) + +--- + +## 🚀 下一步建议 + +1. **清理调试代码** ✅ (已完成) +2. **提交代码到 Git** +3. **迁移其他工作站模块** (可选) +4. **更新文档和启动脚本** + +--- + +**修改完成日期**: 2026-01-13 +**系统状态**: ✅ 稳定运行 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 new file mode 100644 index 00000000..333b7b28 --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py @@ -0,0 +1,2112 @@ +# -*- 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 +# ⚠️ config.py 已废弃 - 所有配置现在从 JSON 文件加载 +# from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, ... +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, bioyond_config: dict = None, deck=None, protocol_type=None, **kwargs): + """ + 初始化 BioyondCellWorkstation + + Args: + bioyond_config: 从 JSON 文件加载的 bioyond 配置字典 + 包含 api_host, api_key, HTTP_host, HTTP_port 等配置 + deck: Deck 配置(可选,会从 JSON 中自动处理) + protocol_type: 协议类型(可选) + **kwargs: 其他参数(如 children 等) + """ + + # ⚠️ 配置验证:确保传入了必需的配置 + if bioyond_config is None: + raise ValueError( + "BioyondCellWorkstation 需要 bioyond_config 参数!\n" + "请在 JSON 配置文件的 config 中添加 bioyond_config 字段,例如:\n" + "\"config\": {\n" + " \"bioyond_config\": {\n" + " \"api_host\": \"http://...\",\n" + " \"api_key\": \"...\",\n" + " ...\n" + " }\n" + "}" + ) + + # 验证 bioyond_config 的类型 + if not isinstance(bioyond_config, dict): + raise ValueError( + f"bioyond_config 必须是字典类型,实际类型: {type(bioyond_config).__name__}" + ) + + # 保存配置 + self.bioyond_config = bioyond_config + + # 验证必需的配置参数 + required_keys = ['api_host', 'api_key', 'HTTP_host', 'HTTP_port', + 'material_type_mappings', 'warehouse_mapping'] + missing_keys = [key for key in required_keys if key not in self.bioyond_config] + if missing_keys: + raise ValueError( + f"bioyond_config 缺少必需参数: {', '.join(missing_keys)}\n" + f"请检查 JSON 配置文件中的 bioyond_config 字段" + ) + + logger.info("✅ 从 JSON 配置加载 bioyond_config 成功") + logger.info(f" API Host: {self.bioyond_config.get('api_host')}") + logger.info(f" HTTP Service: {self.bioyond_config.get('HTTP_host')}:{self.bioyond_config.get('HTTP_port')}") + + # 设置调试模式 + self.debug_mode = self.bioyond_config.get("debug_mode", False) + self.http_service_started = self.debug_mode + self._device_id = "bioyond_cell_workstation" # 默认值,后续会从_ros_node获取 + + # ⚠️ 关键:设置标志位,告诉父类不要在 post_init 中启动 HTTP 服务 + # 因为子类会在这里自己启动 HTTP 服务 + self.bioyond_config["_disable_auto_http_service"] = True + logger.info("🔧 已设置 _disable_auto_http_service 标志,防止 HTTP 服务重复启动") + + # 调用父类初始化(传入完整的 bioyond_config) + super().__init__(bioyond_config=self.bioyond_config, deck=deck, **kwargs) + + # 更新奔耀端的报送 IP 地址 + self.update_push_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"✅ BioyondCellWorkstation 初始化完成 (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"[DEBUG] ========== 收到 order_finish 报送 ==========") + logger.info(f"[DEBUG] 报送的 orderCode: '{order_code}' (type: {type(order_code).__name__})") + logger.info(f"[DEBUG] 当前等待的 last_order_code: '{self.last_order_code}' (type: {type(self.last_order_code).__name__})") + logger.info(f"[DEBUG] 报送状态: {status}") + logger.info(f"[DEBUG] orderCode 是否匹配: {self.last_order_code == order_code}") + logger.info(f"[DEBUG] Event 当前状态 (触发前): is_set={self.order_finish_event.is_set()}") + 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: + logger.info(f"[DEBUG] ✅ orderCode 匹配!触发 order_finish_event") + self.order_finish_event.set() + logger.info(f"[DEBUG] Event 状态 (触发后): is_set={self.order_finish_event.is_set()}") + else: + logger.warning(f"[DEBUG] ❌ orderCode 不匹配,不触发 event") + logger.warning(f"[DEBUG] 期望: '{self.last_order_code}'") + logger.warning(f"[DEBUG] 实际: '{order_code}'") + + logger.info(f"[DEBUG] ========================================") + 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} + + def wait_for_order_finish_polling(self, order_code: str, timeout: int = 36000, poll_interval: float = 0.5) -> Dict[str, Any]: + """ + 等待指定 orderCode 的 /report/order_finish 报送(非阻塞轮询版本)。 + + 与 wait_for_order_finish 的区别: + - 使用轮询而非阻塞等待,每隔 poll_interval 秒检查一次 + - 允许 ROS2 在等待期间处理 feedback 消息 + - 适用于长时间运行的 ROS2 Action + + Args: + order_code: 任务编号 + timeout: 超时时间(秒) + poll_interval: 轮询间隔(秒),默认 0.5 秒 + Returns: + 完整的报送数据 + 状态判断结果 + """ + if not order_code: + logger.error("wait_for_order_finish_polling() 被调用,但 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, poll_interval={poll_interval}s)") + logger.info(f"[轮询模式] [DEBUG] last_order_code 已设置为: '{self.last_order_code}'") + logger.info(f"[轮询模式] [DEBUG] Event 初始状态: is_set={self.order_finish_event.is_set()}") + + start_time = time.time() + poll_count = 0 + while not self.order_finish_event.is_set(): + poll_count += 1 + elapsed = time.time() - start_time + + # 每 10 次轮询(约 5 秒)输出一次状态 + if poll_count % 10 == 0: + logger.info(f"[轮询模式] [DEBUG] 轮询中... 已等待 {elapsed:.1f}s (第{poll_count}次检查)") + logger.info(f"[轮询模式] [DEBUG] Event.is_set() = {self.order_finish_event.is_set()}") + + # 检查是否超时 + if elapsed > timeout: + logger.error(f"[轮询模式] 等待任务超时: orderCode={order_code}") + logger.error(f"[轮询模式] [DEBUG] 总共轮询了 {poll_count} 次,耗时 {elapsed:.1f}s") + return {"status": "timeout", "orderCode": order_code} + + # 短暂 sleep,让出控制权给 ROS2 处理 feedback + time.sleep(poll_interval) + + # 事件已触发,获取报送数据 + logger.info(f"[轮询模式] [DEBUG] ✅ Event 已触发!共轮询 {poll_count} 次") + report = self.last_order_report or {} + report_code = report.get("orderCode") + status = str(report.get("status", "")) + + logger.info(f"[轮询模式] [DEBUG] 报送数据: orderCode='{report_code}', status={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} + + + 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: + 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] = "D:\\UniLab\\Uni-Lab-OS\\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("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] 使用 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}") + + # 提取所有返回的 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] 等待 {len(order_codes)} 个订单完成: {order_codes}") + + # 等待所有订单完成并收集报文 + all_reports = [] + for idx, order_code in enumerate(order_codes, 1): + print(f"[create_orders] 正在等待第 {idx}/{len(order_codes)} 个订单: {order_code}") + result = self.wait_for_order_finish(order_code) + + # 提取报文数据 + 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: + 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] 所有订单已完成,共收集 {len(all_reports)} 个报文") + print("实验记录本========================create_orders========================") + + # 返回所有订单的完成报文 + final_result = { + "status": "all_completed", + "total_orders": len(order_codes), + "reports": all_reports, + "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 + + 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") + # 3.10 停止调度 + def scheduler_stop(self) -> Dict[str, Any]: + + """ + 停止调度 (3.10) + 请求体只包含 apiKey 和 requestTime + """ + return self._post_lims("/api/lims/scheduler/stop") + + # 2.9 继续调度 + def scheduler_continue(self) -> Dict[str, Any]: + """ + 继续调度 (2.9) + 请求体只包含 apiKey 和 requestTime + """ + return self._post_lims("/api/lims/scheduler/continue") + def scheduler_reset(self) -> Dict[str, Any]: + """ + 复位调度 (2.11) + 请求体只包含 apiKey 和 requestTime + """ + return self._post_lims("/api/lims/scheduler/reset") + + def scheduler_start_and_auto_feeding( + self, + # ★ Excel路径参数 + xlsx_path: Optional[str] = "D:\\UniLab\\Uni-Lab-OS\\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, + ) -> Dict[str, Any]: + """ + 组合函数:先启动调度,然后执行自动化上料 + + 此函数简化了工作流操作,将两个有顺序依赖的操作组合在一起: + 1. 启动调度(scheduler_start) + 2. 自动化上料(auto_feeding4to3) + + 参数与 auto_feeding4to3 完全相同,支持 Excel 和手动参数两种模式 + + Returns: + 包含调度启动结果和上料结果的字典 + """ + logger.info("=" * 60) + logger.info("开始执行组合操作:启动调度 + 自动化上料") + logger.info("=" * 60) + + # 步骤1: 启动调度 + logger.info("【步骤 1/2】启动调度...") + scheduler_result = self.scheduler_start() + logger.info(f"调度启动结果: {scheduler_result}") + + # 检查调度是否启动成功 + if scheduler_result.get("code") != 1: + logger.error(f"调度启动失败: {scheduler_result}") + return { + "success": False, + "step": "scheduler_start", + "scheduler_result": scheduler_result, + "error": "调度启动失败" + } + + logger.info("✓ 调度启动成功") + + # 步骤2: 执行自动化上料 + logger.info("【步骤 2/2】执行自动化上料...") + feeding_result = self.auto_feeding4to3( + xlsx_path=xlsx_path, + WH4_x1_y1_z1_1_materialName=WH4_x1_y1_z1_1_materialName, WH4_x1_y1_z1_1_quantity=WH4_x1_y1_z1_1_quantity, + WH4_x2_y1_z1_2_materialName=WH4_x2_y1_z1_2_materialName, WH4_x2_y1_z1_2_quantity=WH4_x2_y1_z1_2_quantity, + WH4_x3_y1_z1_3_materialName=WH4_x3_y1_z1_3_materialName, WH4_x3_y1_z1_3_quantity=WH4_x3_y1_z1_3_quantity, + WH4_x4_y1_z1_4_materialName=WH4_x4_y1_z1_4_materialName, WH4_x4_y1_z1_4_quantity=WH4_x4_y1_z1_4_quantity, + WH4_x5_y1_z1_5_materialName=WH4_x5_y1_z1_5_materialName, WH4_x5_y1_z1_5_quantity=WH4_x5_y1_z1_5_quantity, + WH4_x1_y2_z1_6_materialName=WH4_x1_y2_z1_6_materialName, WH4_x1_y2_z1_6_quantity=WH4_x1_y2_z1_6_quantity, + WH4_x2_y2_z1_7_materialName=WH4_x2_y2_z1_7_materialName, WH4_x2_y2_z1_7_quantity=WH4_x2_y2_z1_7_quantity, + WH4_x3_y2_z1_8_materialName=WH4_x3_y2_z1_8_materialName, WH4_x3_y2_z1_8_quantity=WH4_x3_y2_z1_8_quantity, + WH4_x4_y2_z1_9_materialName=WH4_x4_y2_z1_9_materialName, WH4_x4_y2_z1_9_quantity=WH4_x4_y2_z1_9_quantity, + WH4_x5_y2_z1_10_materialName=WH4_x5_y2_z1_10_materialName, WH4_x5_y2_z1_10_quantity=WH4_x5_y2_z1_10_quantity, + WH4_x1_y3_z1_11_materialName=WH4_x1_y3_z1_11_materialName, WH4_x1_y3_z1_11_quantity=WH4_x1_y3_z1_11_quantity, + WH4_x2_y3_z1_12_materialName=WH4_x2_y3_z1_12_materialName, WH4_x2_y3_z1_12_quantity=WH4_x2_y3_z1_12_quantity, + WH4_x1_y1_z2_1_materialName=WH4_x1_y1_z2_1_materialName, WH4_x1_y1_z2_1_quantity=WH4_x1_y1_z2_1_quantity, + WH4_x1_y1_z2_1_materialType=WH4_x1_y1_z2_1_materialType, WH4_x1_y1_z2_1_targetWH=WH4_x1_y1_z2_1_targetWH, + WH4_x2_y1_z2_2_materialName=WH4_x2_y1_z2_2_materialName, WH4_x2_y1_z2_2_quantity=WH4_x2_y1_z2_2_quantity, + WH4_x2_y1_z2_2_materialType=WH4_x2_y1_z2_2_materialType, WH4_x2_y1_z2_2_targetWH=WH4_x2_y1_z2_2_targetWH, + WH4_x3_y1_z2_3_materialName=WH4_x3_y1_z2_3_materialName, WH4_x3_y1_z2_3_quantity=WH4_x3_y1_z2_3_quantity, + WH4_x3_y1_z2_3_materialType=WH4_x3_y1_z2_3_materialType, WH4_x3_y1_z2_3_targetWH=WH4_x3_y1_z2_3_targetWH, + WH4_x1_y2_z2_4_materialName=WH4_x1_y2_z2_4_materialName, WH4_x1_y2_z2_4_quantity=WH4_x1_y2_z2_4_quantity, + WH4_x1_y2_z2_4_materialType=WH4_x1_y2_z2_4_materialType, WH4_x1_y2_z2_4_targetWH=WH4_x1_y2_z2_4_targetWH, + WH4_x2_y2_z2_5_materialName=WH4_x2_y2_z2_5_materialName, WH4_x2_y2_z2_5_quantity=WH4_x2_y2_z2_5_quantity, + WH4_x2_y2_z2_5_materialType=WH4_x2_y2_z2_5_materialType, WH4_x2_y2_z2_5_targetWH=WH4_x2_y2_z2_5_targetWH, + WH4_x3_y2_z2_6_materialName=WH4_x3_y2_z2_6_materialName, WH4_x3_y2_z2_6_quantity=WH4_x3_y2_z2_6_quantity, + WH4_x3_y2_z2_6_materialType=WH4_x3_y2_z2_6_materialType, WH4_x3_y2_z2_6_targetWH=WH4_x3_y2_z2_6_targetWH, + WH4_x1_y3_z2_7_materialName=WH4_x1_y3_z2_7_materialName, WH4_x1_y3_z2_7_quantity=WH4_x1_y3_z2_7_quantity, + WH4_x1_y3_z2_7_materialType=WH4_x1_y3_z2_7_materialType, WH4_x1_y3_z2_7_targetWH=WH4_x1_y3_z2_7_targetWH, + WH4_x2_y3_z2_8_materialName=WH4_x2_y3_z2_8_materialName, WH4_x2_y3_z2_8_quantity=WH4_x2_y3_z2_8_quantity, + WH4_x2_y3_z2_8_materialType=WH4_x2_y3_z2_8_materialType, WH4_x2_y3_z2_8_targetWH=WH4_x2_y3_z2_8_targetWH, + WH4_x3_y3_z2_9_materialName=WH4_x3_y3_z2_9_materialName, WH4_x3_y3_z2_9_quantity=WH4_x3_y3_z2_9_quantity, + WH4_x3_y3_z2_9_materialType=WH4_x3_y3_z2_9_materialType, WH4_x3_y3_z2_9_targetWH=WH4_x3_y3_z2_9_targetWH, + WH3_x1_y1_z3_1_materialType=WH3_x1_y1_z3_1_materialType, WH3_x1_y1_z3_1_materialId=WH3_x1_y1_z3_1_materialId, WH3_x1_y1_z3_1_quantity=WH3_x1_y1_z3_1_quantity, + WH3_x2_y1_z3_2_materialType=WH3_x2_y1_z3_2_materialType, WH3_x2_y1_z3_2_materialId=WH3_x2_y1_z3_2_materialId, WH3_x2_y1_z3_2_quantity=WH3_x2_y1_z3_2_quantity, + WH3_x3_y1_z3_3_materialType=WH3_x3_y1_z3_3_materialType, WH3_x3_y1_z3_3_materialId=WH3_x3_y1_z3_3_materialId, WH3_x3_y1_z3_3_quantity=WH3_x3_y1_z3_3_quantity, + WH3_x1_y2_z3_4_materialType=WH3_x1_y2_z3_4_materialType, WH3_x1_y2_z3_4_materialId=WH3_x1_y2_z3_4_materialId, WH3_x1_y2_z3_4_quantity=WH3_x1_y2_z3_4_quantity, + WH3_x2_y2_z3_5_materialType=WH3_x2_y2_z3_5_materialType, WH3_x2_y2_z3_5_materialId=WH3_x2_y2_z3_5_materialId, WH3_x2_y2_z3_5_quantity=WH3_x2_y2_z3_5_quantity, + WH3_x3_y2_z3_6_materialType=WH3_x3_y2_z3_6_materialType, WH3_x3_y2_z3_6_materialId=WH3_x3_y2_z3_6_materialId, WH3_x3_y2_z3_6_quantity=WH3_x3_y2_z3_6_quantity, + WH3_x1_y3_z3_7_materialType=WH3_x1_y3_z3_7_materialType, WH3_x1_y3_z3_7_materialId=WH3_x1_y3_z3_7_materialId, WH3_x1_y3_z3_7_quantity=WH3_x1_y3_z3_7_quantity, + WH3_x2_y3_z3_8_materialType=WH3_x2_y3_z3_8_materialType, WH3_x2_y3_z3_8_materialId=WH3_x2_y3_z3_8_materialId, WH3_x2_y3_z3_8_quantity=WH3_x2_y3_z3_8_quantity, + WH3_x3_y3_z3_9_materialType=WH3_x3_y3_z3_9_materialType, WH3_x3_y3_z3_9_materialId=WH3_x3_y3_z3_9_materialId, WH3_x3_y3_z3_9_quantity=WH3_x3_y3_z3_9_quantity, + WH3_x1_y4_z3_10_materialType=WH3_x1_y4_z3_10_materialType, WH3_x1_y4_z3_10_materialId=WH3_x1_y4_z3_10_materialId, WH3_x1_y4_z3_10_quantity=WH3_x1_y4_z3_10_quantity, + WH3_x2_y4_z3_11_materialType=WH3_x2_y4_z3_11_materialType, WH3_x2_y4_z3_11_materialId=WH3_x2_y4_z3_11_materialId, WH3_x2_y4_z3_11_quantity=WH3_x2_y4_z3_11_quantity, + WH3_x3_y4_z3_12_materialType=WH3_x3_y4_z3_12_materialType, WH3_x3_y4_z3_12_materialId=WH3_x3_y4_z3_12_materialId, WH3_x3_y4_z3_12_quantity=WH3_x3_y4_z3_12_quantity, + WH3_x1_y5_z3_13_materialType=WH3_x1_y5_z3_13_materialType, WH3_x1_y5_z3_13_materialId=WH3_x1_y5_z3_13_materialId, WH3_x1_y5_z3_13_quantity=WH3_x1_y5_z3_13_quantity, + WH3_x2_y5_z3_14_materialType=WH3_x2_y5_z3_14_materialType, WH3_x2_y5_z3_14_materialId=WH3_x2_y5_z3_14_materialId, WH3_x2_y5_z3_14_quantity=WH3_x2_y5_z3_14_quantity, + WH3_x3_y5_z3_15_materialType=WH3_x3_y5_z3_15_materialType, WH3_x3_y5_z3_15_materialId=WH3_x3_y5_z3_15_materialId, WH3_x3_y5_z3_15_quantity=WH3_x3_y5_z3_15_quantity, + ) + + logger.info("=" * 60) + logger.info("组合操作完成") + logger.info("=" * 60) + + return { + "success": True, + "scheduler_result": scheduler_result, + "feeding_result": feeding_result + } + + + def scheduler_start_and_auto_feeding_v2( + self, + # ★ Excel路径参数 + xlsx_path: Optional[str] = "D:\\UniLab\\Uni-Lab-OS\\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, + ) -> Dict[str, Any]: + """ + 组合函数 V2 版本(测试版):先启动调度,然后执行自动化上料 + + ⚠️ 这是测试版本,使用非阻塞轮询等待方式,避免 ROS2 Action feedback publisher 失效 + + 与 V1 的区别: + - 使用 wait_for_order_finish_polling 替代原有的阻塞等待 + - 允许 ROS2 在等待期间正常发布 feedback 消息 + - 适用于长时间运行的任务 + + 参数与 scheduler_start_and_auto_feeding 完全相同 + + Returns: + 包含调度启动结果和上料结果的字典 + """ + logger.info("=" * 60) + logger.info("[V2测试版本] 开始执行组合操作:启动调度 + 自动化上料") + logger.info("=" * 60) + + # 步骤1: 启动调度 + logger.info("【步骤 1/2】启动调度...") + scheduler_result = self.scheduler_start() + logger.info(f"调度启动结果: {scheduler_result}") + + # 检查调度是否启动成功 + if scheduler_result.get("code") != 1: + logger.error(f"调度启动失败: {scheduler_result}") + return { + "success": False, + "step": "scheduler_start", + "scheduler_result": scheduler_result, + "error": "调度启动失败" + } + + logger.info("✓ 调度启动成功") + + # 步骤2: 执行自动化上料(这里会调用 auto_feeding4to3,内部使用轮询等待) + logger.info("【步骤 2/2】执行自动化上料...") + + # 临时替换 wait_for_order_finish 为轮询版本 + original_wait_func = self.wait_for_order_finish + self.wait_for_order_finish = self.wait_for_order_finish_polling + + try: + feeding_result = self.auto_feeding4to3( + xlsx_path=xlsx_path, + WH4_x1_y1_z1_1_materialName=WH4_x1_y1_z1_1_materialName, WH4_x1_y1_z1_1_quantity=WH4_x1_y1_z1_1_quantity, + WH4_x2_y1_z1_2_materialName=WH4_x2_y1_z1_2_materialName, WH4_x2_y1_z1_2_quantity=WH4_x2_y1_z1_2_quantity, + WH4_x3_y1_z1_3_materialName=WH4_x3_y1_z1_3_materialName, WH4_x3_y1_z1_3_quantity=WH4_x3_y1_z1_3_quantity, + WH4_x4_y1_z1_4_materialName=WH4_x4_y1_z1_4_materialName, WH4_x4_y1_z1_4_quantity=WH4_x4_y1_z1_4_quantity, + WH4_x5_y1_z1_5_materialName=WH4_x5_y1_z1_5_materialName, WH4_x5_y1_z1_5_quantity=WH4_x5_y1_z1_5_quantity, + WH4_x1_y2_z1_6_materialName=WH4_x1_y2_z1_6_materialName, WH4_x1_y2_z1_6_quantity=WH4_x1_y2_z1_6_quantity, + WH4_x2_y2_z1_7_materialName=WH4_x2_y2_z1_7_materialName, WH4_x2_y2_z1_7_quantity=WH4_x2_y2_z1_7_quantity, + WH4_x3_y2_z1_8_materialName=WH4_x3_y2_z1_8_materialName, WH4_x3_y2_z1_8_quantity=WH4_x3_y2_z1_8_quantity, + WH4_x4_y2_z1_9_materialName=WH4_x4_y2_z1_9_materialName, WH4_x4_y2_z1_9_quantity=WH4_x4_y2_z1_9_quantity, + WH4_x5_y2_z1_10_materialName=WH4_x5_y2_z1_10_materialName, WH4_x5_y2_z1_10_quantity=WH4_x5_y2_z1_10_quantity, + WH4_x1_y3_z1_11_materialName=WH4_x1_y3_z1_11_materialName, WH4_x1_y3_z1_11_quantity=WH4_x1_y3_z1_11_quantity, + WH4_x2_y3_z1_12_materialName=WH4_x2_y3_z1_12_materialName, WH4_x2_y3_z1_12_quantity=WH4_x2_y3_z1_12_quantity, + WH4_x1_y1_z2_1_materialName=WH4_x1_y1_z2_1_materialName, WH4_x1_y1_z2_1_quantity=WH4_x1_y1_z2_1_quantity, + WH4_x1_y1_z2_1_materialType=WH4_x1_y1_z2_1_materialType, WH4_x1_y1_z2_1_targetWH=WH4_x1_y1_z2_1_targetWH, + WH4_x2_y1_z2_2_materialName=WH4_x2_y1_z2_2_materialName, WH4_x2_y1_z2_2_quantity=WH4_x2_y1_z2_2_quantity, + WH4_x2_y1_z2_2_materialType=WH4_x2_y1_z2_2_materialType, WH4_x2_y1_z2_2_targetWH=WH4_x2_y1_z2_2_targetWH, + WH4_x3_y1_z2_3_materialName=WH4_x3_y1_z2_3_materialName, WH4_x3_y1_z2_3_quantity=WH4_x3_y1_z2_3_quantity, + WH4_x3_y1_z2_3_materialType=WH4_x3_y1_z2_3_materialType, WH4_x3_y1_z2_3_targetWH=WH4_x3_y1_z2_3_targetWH, + WH4_x1_y2_z2_4_materialName=WH4_x1_y2_z2_4_materialName, WH4_x1_y2_z2_4_quantity=WH4_x1_y2_z2_4_quantity, + WH4_x1_y2_z2_4_materialType=WH4_x1_y2_z2_4_materialType, WH4_x1_y2_z2_4_targetWH=WH4_x1_y2_z2_4_targetWH, + WH4_x2_y2_z2_5_materialName=WH4_x2_y2_z2_5_materialName, WH4_x2_y2_z2_5_quantity=WH4_x2_y2_z2_5_quantity, + WH4_x2_y2_z2_5_materialType=WH4_x2_y2_z2_5_materialType, WH4_x2_y2_z2_5_targetWH=WH4_x2_y2_z2_5_targetWH, + WH4_x3_y2_z2_6_materialName=WH4_x3_y2_z2_6_materialName, WH4_x3_y2_z2_6_quantity=WH4_x3_y2_z2_6_quantity, + WH4_x3_y2_z2_6_materialType=WH4_x3_y2_z2_6_materialType, WH4_x3_y2_z2_6_targetWH=WH4_x3_y2_z2_6_targetWH, + WH4_x1_y3_z2_7_materialName=WH4_x1_y3_z2_7_materialName, WH4_x1_y3_z2_7_quantity=WH4_x1_y3_z2_7_quantity, + WH4_x1_y3_z2_7_materialType=WH4_x1_y3_z2_7_materialType, WH4_x1_y3_z2_7_targetWH=WH4_x1_y3_z2_7_targetWH, + WH4_x2_y3_z2_8_materialName=WH4_x2_y3_z2_8_materialName, WH4_x2_y3_z2_8_quantity=WH4_x2_y3_z2_8_quantity, + WH4_x2_y3_z2_8_materialType=WH4_x2_y3_z2_8_materialType, WH4_x2_y3_z2_8_targetWH=WH4_x2_y3_z2_8_targetWH, + WH4_x3_y3_z2_9_materialName=WH4_x3_y3_z2_9_materialName, WH4_x3_y3_z2_9_quantity=WH4_x3_y3_z2_9_quantity, + WH4_x3_y3_z2_9_materialType=WH4_x3_y3_z2_9_materialType, WH4_x3_y3_z2_9_targetWH=WH4_x3_y3_z2_9_targetWH, + WH3_x1_y1_z3_1_materialType=WH3_x1_y1_z3_1_materialType, WH3_x1_y1_z3_1_materialId=WH3_x1_y1_z3_1_materialId, WH3_x1_y1_z3_1_quantity=WH3_x1_y1_z3_1_quantity, + WH3_x2_y1_z3_2_materialType=WH3_x2_y1_z3_2_materialType, WH3_x2_y1_z3_2_materialId=WH3_x2_y1_z3_2_materialId, WH3_x2_y1_z3_2_quantity=WH3_x2_y1_z3_2_quantity, + WH3_x3_y1_z3_3_materialType=WH3_x3_y1_z3_3_materialType, WH3_x3_y1_z3_3_materialId=WH3_x3_y1_z3_3_materialId, WH3_x3_y1_z3_3_quantity=WH3_x3_y1_z3_3_quantity, + WH3_x1_y2_z3_4_materialType=WH3_x1_y2_z3_4_materialType, WH3_x1_y2_z3_4_materialId=WH3_x1_y2_z3_4_materialId, WH3_x1_y2_z3_4_quantity=WH3_x1_y2_z3_4_quantity, + WH3_x2_y2_z3_5_materialType=WH3_x2_y2_z3_5_materialType, WH3_x2_y2_z3_5_materialId=WH3_x2_y2_z3_5_materialId, WH3_x2_y2_z3_5_quantity=WH3_x2_y2_z3_5_quantity, + WH3_x3_y2_z3_6_materialType=WH3_x3_y2_z3_6_materialType, WH3_x3_y2_z3_6_materialId=WH3_x3_y2_z3_6_materialId, WH3_x3_y2_z3_6_quantity=WH3_x3_y2_z3_6_quantity, + WH3_x1_y3_z3_7_materialType=WH3_x1_y3_z3_7_materialType, WH3_x1_y3_z3_7_materialId=WH3_x1_y3_z3_7_materialId, WH3_x1_y3_z3_7_quantity=WH3_x1_y3_z3_7_quantity, + WH3_x2_y3_z3_8_materialType=WH3_x2_y3_z3_8_materialType, WH3_x2_y3_z3_8_materialId=WH3_x2_y3_z3_8_materialId, WH3_x2_y3_z3_8_quantity=WH3_x2_y3_z3_8_quantity, + WH3_x3_y3_z3_9_materialType=WH3_x3_y3_z3_9_materialType, WH3_x3_y3_z3_9_materialId=WH3_x3_y3_z3_9_materialId, WH3_x3_y3_z3_9_quantity=WH3_x3_y3_z3_9_quantity, + WH3_x1_y4_z3_10_materialType=WH3_x1_y4_z3_10_materialType, WH3_x1_y4_z3_10_materialId=WH3_x1_y4_z3_10_materialId, WH3_x1_y4_z3_10_quantity=WH3_x1_y4_z3_10_quantity, + WH3_x2_y4_z3_11_materialType=WH3_x2_y4_z3_11_materialType, WH3_x2_y4_z3_11_materialId=WH3_x2_y4_z3_11_materialId, WH3_x2_y4_z3_11_quantity=WH3_x2_y4_z3_11_quantity, + WH3_x3_y4_z3_12_materialType=WH3_x3_y4_z3_12_materialType, WH3_x3_y4_z3_12_materialId=WH3_x3_y4_z3_12_materialId, WH3_x3_y4_z3_12_quantity=WH3_x3_y4_z3_12_quantity, + WH3_x1_y5_z3_13_materialType=WH3_x1_y5_z3_13_materialType, WH3_x1_y5_z3_13_materialId=WH3_x1_y5_z3_13_materialId, WH3_x1_y5_z3_13_quantity=WH3_x1_y5_z3_13_quantity, + WH3_x2_y5_z3_14_materialType=WH3_x2_y5_z3_14_materialType, WH3_x2_y5_z3_14_materialId=WH3_x2_y5_z3_14_materialId, WH3_x2_y5_z3_14_quantity=WH3_x2_y5_z3_14_quantity, + WH3_x3_y5_z3_15_materialType=WH3_x3_y5_z3_15_materialType, WH3_x3_y5_z3_15_materialId=WH3_x3_y5_z3_15_materialId, WH3_x3_y5_z3_15_quantity=WH3_x3_y5_z3_15_quantity, + ) + finally: + # 恢复原有函数 + self.wait_for_order_finish = original_wait_func + + logger.info("=" * 60) + logger.info("[V2测试版本] 组合操作完成") + logger.info("=" * 60) + + return { + "success": True, + "scheduler_result": scheduler_result, + "feeding_result": feeding_result, + "version": "v2_polling" + } + + + # 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 + + 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]: + """ + 1→2 物料转运 + 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") + 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(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过滤关键字查询) + 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() + # 优先从配置中获取模板数据 + template = self.bioyond_config.get('solid_liquid_mappings', {}).get(material_name) + if not template: + raise ValueError(f"在配置中未找到物料 {material_name} 的模板,请检查 bioyond_config.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 self.bioyond_config['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 self.bioyond_config['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 = self.bioyond_config['material_type_mappings'][board_type][1] + bottle_type_id = self.bioyond_config['material_type_mappings'][bottle_type][1] + + # 从指定仓库获取库位UUID + if warehouse_name not in self.bioyond_config['warehouse_mapping']: + logger.error(f"未找到仓库: {warehouse_name},回退到手动堆栈") + warehouse_name = "手动堆栈" + + if location_code not in self.bioyond_config['warehouse_mapping'][warehouse_name]["site_uuids"]: + logger.error(f"仓库 {warehouse_name} 中未找到库位 {location_code}") + raise ValueError(f"库位 {location_code} 在仓库 {warehouse_name} 中不存在") + + location_id = self.bioyond_config['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/material_template.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx new file mode 100644 index 00000000..88b233da Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx differ diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/outbound_template.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/outbound_template.xlsx new file mode 100644 index 00000000..f2c42f25 Binary files /dev/null and b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/outbound_template.xlsx differ diff --git "a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/\346\211\271\351\207\217\345\207\272\345\272\223\346\250\241\346\235\277\344\275\277\347\224\250\350\257\264\346\230\216.md" "b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/\346\211\271\351\207\217\345\207\272\345\272\223\346\250\241\346\235\277\344\275\277\347\224\250\350\257\264\346\230\216.md" new file mode 100644 index 00000000..5d11c7ba --- /dev/null +++ "b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/\346\211\271\351\207\217\345\207\272\345\272\223\346\250\241\346\235\277\344\275\277\347\224\250\350\257\264\346\230\216.md" @@ -0,0 +1,157 @@ +# 批量出库 Excel 模板使用说明 + +**文件**: `outbound_template.xlsx` +**用途**: 配合 `auto_batch_outbound_from_xlsx()` 方法进行批量出库操作 +**API 端点**: `/api/lims/storage/auto-batch-out-bound` + +--- + +## 📋 Excel 列说明 + +| 列名 | 说明 | 示例 | 必填 | +|------|------|------|------| +| `locationId` | **库位 ID(UUID)** | `3a19da43-57b5-294f-d663-154a1cc32270` | ✅ 是 | +| `warehouseId` | **仓库 ID 或名称** | `配液站内试剂仓库` | ✅ 是 | +| `quantity` | **出库数量** | `1.0`, `2.0` | ✅ 是 | +| `x` | **X 坐标(库位横向位置)** | `1`, `2`, `3` | ✅ 是 | +| `y` | **Y 坐标(库位纵向位置)** | `1`, `2`, `3` | ✅ 是 | +| `z` | **Z 坐标(库位层数/高度)** | `1`, `2`, `3` | ✅ 是 | +| `备注说明` | 可选备注信息 | `配液站内试剂仓库-A01` | ❌ 否 | + +### 📐 坐标说明 + +**x, y, z** 是库位在仓库内的**三维坐标**: + +``` +仓库(例如 WH4) +├── Z=1(第1层/加样头面) +│ ├── X=1, Y=1(位置 A) +│ ├── X=2, Y=1(位置 B) +│ ├── X=3, Y=1(位置 C) +│ └── ... +│ +└── Z=2(第2层/原液瓶面) + ├── X=1, Y=1(位置 A) + ├── X=2, Y=1(位置 B) + └── ... +``` + +- **warehouseId**: 指定哪个仓库(WH3, WH4, 配液站等) +- **x, y, z**: 在该仓库内的三维坐标 +- **locationId**: 该坐标位置的唯一 UUID + +### 🎯 起点与终点 + +**重要说明**:批量出库模板**只规定了出库的"起点"**(从哪里取物料),**没有指定"终点"**(放到哪里)。 + +``` +出库流程: +起点(Excel 指定) → ?终点(LIMS/工作流决定) + ↓ +locationId, x, y, z → 由 LIMS 系统或当前工作流自动分配 +``` + +**终点由以下方式确定:** +- **LIMS 系统自动分配**:根据当前任务自动规划目标位置 +- **工作流预定义**:在创建出库任务时已绑定目标位置 +- **暂存区**:默认放到出库暂存区,等待下一步操作 + +💡 **对比**:上料操作(`auto_feeding4to3`)则有 `targetWH` 参数可以指定目标仓库 + +--- + +## 🔍 如何获取 UUID? + +### 方法 1:从配置文件获取 + +参考 `yibin_electrolyte_config.json` 中的 `warehouse_mapping`: + +```json +{ + "warehouse_mapping": { + "配液站内试剂仓库": { + "site_uuids": { + "A01": "3a19da43-57b5-294f-d663-154a1cc32270", + "B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2", + "C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f" + } + }, + "手动堆栈": { + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", + "A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe" + } + } + } +} +``` + +### 方法 2:通过 API 查询 + +```python +material_info = hardware_interface.material_id_query(workflow_id) +locations = material_info.get("locations", []) +``` + +--- + +## 📝 填写示例 + +### 示例 1:从配液站内试剂仓库出库 + +| locationId | warehouseId | quantity | x | y | z | 备注说明 | +|------------|-------------|----------|---|---|---|----------| +| `3a19da43-57b5-294f-d663-154a1cc32270` | 配液站内试剂仓库 | 1 | 1 | 1 | 1 | A01 位置 | +| `3a19da43-57b5-7394-5f49-54efe2c9bef2` | 配液站内试剂仓库 | 2 | 2 | 1 | 1 | B01 位置 | + +### 示例 2:从手动堆栈出库 + +| locationId | warehouseId | quantity | x | y | z | 备注说明 | +|------------|-------------|----------|---|---|---|----------| +| `3a19deae-2c7a-36f5-5e41-02c5b66feaea` | 手动堆栈 | 1 | 1 | 1 | 1 | A01 | +| `3a19deae-2c7a-dc6d-c41e-ef285d946cfe` | 手动堆栈 | 1 | 1 | 2 | 1 | A02 | + +--- + +## 💻 使用方法 + +```python +from bioyond_cell_workstation import BioyondCellWorkstation + +# 初始化工作站 +workstation = BioyondCellWorkstation(config=config, deck=deck) + +# 调用批量出库方法 +result = workstation.auto_batch_outbound_from_xlsx( + xlsx_path="outbound_template.xlsx" +) +``` + +--- + +## ⚠️ 注意事项 + +1. **locationId 必须是有效的 UUID**,不能使用库位名称 +2. **x, y, z 坐标必须与 locationId 对应**,表示该库位在仓库内的位置 +3. **quantity 必须是数字**,可以是整数或浮点数 +4. Excel 文件必须包含表头行 +5. 空行会被自动跳过 +6. 确保 UUID 与实际库位对应,否则 API 会报错 + +--- + +## 📚 相关文件 + +- **配置文件**: `yibin_electrolyte_config.json` +- **Python 代码**: `bioyond_cell_workstation.py` (L630-695) +- **生成脚本**: `create_outbound_template.py` +- **上料模板**: `material_template.xlsx` + +--- + +## 🔄 重新生成模板 + +```bash +conda activate newunilab +python create_outbound_template.py +``` diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py index afd515aa..c365be7a 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -9,7 +9,7 @@ from unilabos.device_comms.rpc import BaseRequest from typing import Optional, List, Dict, Any import json -from unilabos.devices.workstation.bioyond_studio.config import LOCATION_MAPPING + class SimpleLogger: @@ -49,6 +49,14 @@ def __init__(self, config): self.config = config self.api_key = config["api_key"] self.host = config["api_host"] + + # 初始化 location_mapping + # 直接从 warehouse_mapping 构建,确保数据源所谓的单一和结构化 + self.location_mapping = {} + warehouse_mapping = self.config.get("warehouse_mapping", {}) + for warehouse_name, warehouse_config in warehouse_mapping.items(): + if "site_uuids" in warehouse_config: + self.location_mapping.update(warehouse_config["site_uuids"]) self._logger = SimpleLogger() self.material_cache = {} self._load_material_cache() @@ -176,7 +184,40 @@ def add_material(self, params: dict) -> dict: return {} print(f"add material data: {response['data']}") - return response.get("data", {}) + + # 自动更新缓存 + data = response.get("data", {}) + if data: + if isinstance(data, str): + # 如果返回的是字符串,通常是ID + mat_id = data + name = params.get("name") + else: + # 如果返回的是字典,尝试获取name和id + name = data.get("name") or params.get("name") + mat_id = data.get("id") + + if name and mat_id: + self.material_cache[name] = mat_id + print(f"已自动更新缓存: {name} -> {mat_id}") + + # 处理返回数据中的 details (如果有) + # 有些 API 返回结构可能直接包含 details,或者在 data 字段中 + details = data.get("details", []) if isinstance(data, dict) else [] + if not details and isinstance(data, dict): + details = data.get("detail", []) + + if details: + for detail in details: + d_name = detail.get("name") + # 尝试从不同字段获取 ID + d_id = detail.get("id") or detail.get("detailMaterialId") + + if d_name and d_id: + self.material_cache[d_name] = d_id + print(f"已自动更新 detail 缓存: {d_name} -> {d_id}") + + return data def query_matial_type_id(self, data) -> list: """查找物料typeid""" @@ -203,7 +244,7 @@ def material_type_list(self) -> list: params={ "apiKey": self.api_key, "requestTime": self.get_current_time_iso8601(), - "data": {}, + "data": 0, }) if not response or response['code'] != 1: return [] @@ -273,11 +314,19 @@ def delete_material(self, material_id: str) -> dict: if not response or response['code'] != 1: return {} + + # 自动更新缓存 - 移除被删除的物料 + for name, mid in list(self.material_cache.items()): + if mid == material_id: + del self.material_cache[name] + print(f"已从缓存移除物料: {name}") + break + return response.get("data", {}) def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict: """指定库位出库物料(通过库位名称)""" - location_id = LOCATION_MAPPING.get(location_name, location_name) + location_id = self.location_mapping.get(location_name, location_name) params = { "materialId": material_id, @@ -1103,6 +1152,10 @@ def _load_material_cache(self): for detail_material in detail_materials: detail_name = detail_material.get("name") detail_id = detail_material.get("detailMaterialId") + if not detail_id: + # 尝试其他可能的字段 + detail_id = detail_material.get("id") + if detail_name and detail_id: self.material_cache[detail_name] = detail_id print(f"加载detail材料: {detail_name} -> ID: {detail_id}") @@ -1123,6 +1176,14 @@ def _get_material_id_by_name(self, material_name_or_id: str) -> str: print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}") return material_id + # 如果缓存中没有,尝试刷新缓存 + print(f"缓存中未找到材料 '{material_name_or_id}',尝试刷新缓存...") + self.refresh_material_cache() + if material_name_or_id in self.material_cache: + material_id = self.material_cache[material_name_or_id] + print(f"刷新缓存后找到材料: {material_name_or_id} -> ID: {material_id}") + return material_id + print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值") return material_name_or_id diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py deleted file mode 100644 index e06c4135..00000000 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ /dev/null @@ -1,142 +0,0 @@ -# config.py -""" -配置文件 - 包含所有配置信息和映射关系 -""" - -# API配置 -API_CONFIG = { - "api_key": "", - "api_host": "" -} - -# 工作流映射配置 -WORKFLOW_MAPPINGS = { - "reactor_taken_out": "", - "reactor_taken_in": "", - "Solid_feeding_vials": "", - "Liquid_feeding_vials(non-titration)": "", - "Liquid_feeding_solvents": "", - "Liquid_feeding(titration)": "", - "liquid_feeding_beaker": "", - "Drip_back": "", -} - -# 工作流名称到DisplaySectionName的映射 -WORKFLOW_TO_SECTION_MAP = { - 'reactor_taken_in': '反应器放入', - 'liquid_feeding_beaker': '液体投料-烧杯', - 'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)', - 'Liquid_feeding_solvents': '液体投料-溶剂', - 'Solid_feeding_vials': '固体投料-小瓶', - 'Liquid_feeding(titration)': '液体投料-滴定', - 'reactor_taken_out': '反应器取出' -} - -# 库位映射配置 -WAREHOUSE_MAPPING = { - "粉末堆栈": { - "uuid": "", - "site_uuids": { - # 样品板 - "A1": "3a14198e-6929-31f0-8a22-0f98f72260df", - "A2": "3a14198e-6929-4379-affa-9a2935c17f99", - "A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af", - "A4": "3a14198e-6929-5e99-2b79-80720f7cfb54", - "B1": "3a14198e-6929-f525-9a1b-1857552b28ee", - "B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d", - "B3": "3a14198e-6929-2d86-a468-602175a2b5aa", - "B4": "3a14198e-6929-1a98-ae57-e97660c489ad", - # 分装板 - "C1": "3a14198e-6929-46fe-841e-03dd753f1e4a", - "C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95", - "C3": "3a14198e-6929-72ac-32ce-9b50245682b8", - "C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118", - "D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2", - "D2": "3a14198e-6929-dde1-fc78-34a84b71afdf", - "D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963", - "D4": "3a14198e-6929-7ac8-915a-fea51cb2e884" - } - }, - "溶液堆栈": { - "uuid": "", - "site_uuids": { - "A1": "3a14198e-d724-e036-afdc-2ae39a7f3383", - "A2": "3a14198e-d724-afa4-fc82-0ac8a9016791", - "A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6", - "A4": "3a14198e-d724-df6d-5e32-5483b3cab583", - "B1": "3a14198e-d724-d818-6d4f-5725191a24b5", - "B2": "3a14198e-d724-be8a-5e0b-012675e195c6", - "B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8", - "B4": "3a14198e-d724-1e28-c885-574c3df468d0", - "C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31", - "C2": "3a14198e-d724-ab4e-48cb-817c3c146707", - "C3": "3a14198e-d724-7f18-1853-39d0c62e1d33", - "C4": "3a14198e-d724-28a2-a760-baa896f46b66", - "D1": "3a14198e-d724-d378-d266-2508a224a19f", - "D2": "3a14198e-d724-f56e-468b-0110a8feb36a", - "D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c", - "D4": "3a14198e-d724-0ddd-9654-f9352a421de9" - } - }, - "试剂堆栈": { - "uuid": "", - "site_uuids": { - "A1": "3a14198c-c2cf-8b40-af28-b467808f1c36", - "A2": "3a14198c-c2d0-f3e7-871a-e470d144296f", - "A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094", - "A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f", - "B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8", - "B2": "3a14198c-c2d0-1559-105d-0ea30682cab4", - "B3": "3a14198c-c2d0-725e-523d-34c037ac2440", - "B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39" - } - } -} - -# 物料类型配置 -MATERIAL_TYPE_MAPPINGS = { - "烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), - "试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""), - "样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"), - "分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), - "样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), - "90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), - "10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"), -} - -# 步骤参数配置(各工作流的步骤UUID) -WORKFLOW_STEP_IDS = { - "reactor_taken_in": { - "config": "" - }, - "liquid_feeding_beaker": { - "liquid": "", - "observe": "" - }, - "liquid_feeding_vials_non_titration": { - "liquid": "", - "observe": "" - }, - "liquid_feeding_solvents": { - "liquid": "", - "observe": "" - }, - "solid_feeding_vials": { - "feeding": "", - "observe": "" - }, - "liquid_feeding_titration": { - "liquid": "", - "observe": "" - }, - "drip_back": { - "liquid": "", - "observe": "" - } -} - -LOCATION_MAPPING = {} - -ACTION_NAMES = {} - -HTTP_SERVICE_CONFIG = {} \ No newline at end of file diff --git a/unilabos/devices/workstation/bioyond_studio/config.py.deprecated b/unilabos/devices/workstation/bioyond_studio/config.py.deprecated new file mode 100644 index 00000000..cccd0873 --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/config.py.deprecated @@ -0,0 +1,329 @@ +# config.py +""" +Bioyond工作站配置文件 +包含API配置、工作流映射、物料类型映射、仓库库位映射等所有配置信息 +""" + +from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck + +# ============================================================================ +# 基础配置 +# ============================================================================ + +# API配置 +API_CONFIG = { + "api_key": "DE9BDDA0", + "api_host": "http://192.168.1.200:44402" +} + +# HTTP 报送服务配置 +HTTP_SERVICE_CONFIG = { + "http_service_host": "127.0.0.1", # 监听地址 + "http_service_port": 8080, # 监听端口 +} + +# Deck配置 - 反应站工作台配置 +DECK_CONFIG = BIOYOND_PolymerReactionStation_Deck(setup=True) + +# ============================================================================ +# 工作流配置 +# ============================================================================ + +# 工作流ID映射 +WORKFLOW_MAPPINGS = { + "reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1", + "reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6", + "Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6", + "Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47", + "Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046", + "Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6", + "liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", + "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a", +} + +# 工作流名称到显示名称的映射 +WORKFLOW_TO_SECTION_MAP = { + 'reactor_taken_in': '反应器放入', + 'reactor_taken_out': '反应器取出', + 'Solid_feeding_vials': '固体投料-小瓶', + 'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)', + 'Liquid_feeding_solvents': '液体投料-溶剂', + 'Liquid_feeding(titration)': '液体投料-滴定', + 'liquid_feeding_beaker': '液体投料-烧杯', + 'Drip_back': '液体回滴' +} + +# 工作流步骤ID配置 +WORKFLOW_STEP_IDS = { + "reactor_taken_in": { + "config": "60a06f85-c5b3-29eb-180f-4f62dd7e2154" + }, + "liquid_feeding_beaker": { + "liquid": "6808cda7-fee7-4092-97f0-5f9c2ffa60e3", + "observe": "1753c0de-dffc-4ee6-8458-805a2e227362" + }, + "liquid_feeding_vials_non_titration": { + "liquid": "62ea6e95-3d5d-43db-bc1e-9a1802673861", + "observe": "3a167d99-6172-b67b-5f22-a7892197142e" + }, + "liquid_feeding_solvents": { + "liquid": "1fcea355-2545-462b-b727-350b69a313bf", + "observe": "0553dfb3-9ac5-4ace-8e00-2f11029919a8" + }, + "solid_feeding_vials": { + "feeding": "f7ae7448-4f20-4c1d-8096-df6fbadd787a", + "observe": "263c7ed5-7277-426b-bdff-d6fbf77bcc05" + }, + "liquid_feeding_titration": { + "liquid": "a00ec41b-e666-4422-9c20-bfcd3cd15c54", + "observe": "ac738ff6-4c58-4155-87b1-d6f65a2c9ab5" + }, + "drip_back": { + "liquid": "371be86a-ab77-4769-83e5-54580547c48a", + "observe": "ce024b9d-bd20-47b8-9f78-ca5ce7f44cf1" + } +} + +# 工作流动作名称配置 +ACTION_NAMES = { + "reactor_taken_in": { + "config": "通量-配置", + "stirring": "反应模块-开始搅拌" + }, + "solid_feeding_vials": { + "feeding": "粉末加样模块-投料", + "observe": "反应模块-观察搅拌结果" + }, + "liquid_feeding_vials_non_titration": { + "liquid": "稀释液瓶加液位-液体投料", + "observe": "反应模块-滴定结果观察" + }, + "liquid_feeding_solvents": { + "liquid": "试剂AB放置位-试剂吸液分液", + "observe": "反应模块-观察搅拌结果" + }, + "liquid_feeding_titration": { + "liquid": "稀释液瓶加液位-稀释液吸液分液", + "observe": "反应模块-滴定结果观察" + }, + "liquid_feeding_beaker": { + "liquid": "烧杯溶液放置位-烧杯吸液分液", + "observe": "反应模块-观察搅拌结果" + }, + "drip_back": { + "liquid": "试剂AB放置位-试剂吸液分液", + "observe": "反应模块-向下滴定结果观察" + } +} + +# ============================================================================ +# 仓库配置 +# ============================================================================ +# 说明: +# - 出库和入库操作都需要UUID +WAREHOUSE_MAPPING = { + # ========== 反应站仓库 ========== + + # 堆栈1左 - 反应站左侧堆栈 (4行×4列=16个库位, A01~D04) + "堆栈1左": { + "uuid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366", + "site_uuids": { + "A01": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3", + "A02": "3a14aa17-0d49-4bc5-8836-517b75473f5f", + "A03": "3a14aa17-0d49-c2bc-6222-5cee8d2d94f8", + "A04": "3a14aa17-0d49-3ce2-8e9a-008c38d116fb", + "B01": "3a14aa17-0d49-f49c-6b66-b27f185a3b32", + "B02": "3a14aa17-0d49-cf46-df85-a979c9c9920c", + "B03": "3a14aa17-0d49-7698-4a23-f7ffb7d48ba3", + "B04": "3a14aa17-0d49-1231-99be-d5870e6478e9", + "C01": "3a14aa17-0d49-be34-6fae-4aed9d48b70b", + "C02": "3a14aa17-0d49-11d7-0897-34921dcf6b7c", + "C03": "3a14aa17-0d49-9840-0bd5-9c63c1bb2c29", + "C04": "3a14aa17-0d49-8335-3bff-01da69ea4911", + "D01": "3a14aa17-0d49-2bea-c8e5-2b32094935d5", + "D02": "3a14aa17-0d49-cff4-e9e8-5f5f0bc1ef32", + "D03": "3a14aa17-0d49-4948-cb0a-78f30d1ca9b8", + "D04": "3a14aa17-0d49-fd2f-9dfb-a29b11e84099", + }, + }, + + # 堆栈1右 - 反应站右侧堆栈 (4行×4列=16个库位, A05~D08) + "堆栈1右": { + "uuid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366", + "site_uuids": { + "A05": "3a14aa17-0d49-2c61-edc8-72a8ca7192dd", + "A06": "3a14aa17-0d49-60c8-2b00-40b17198f397", + "A07": "3a14aa17-0d49-ec5b-0b75-634dce8eed25", + "A08": "3a14aa17-0d49-3ec9-55b3-f3189c4ec53d", + "B05": "3a14aa17-0d49-6a4e-abcf-4c113eaaeaad", + "B06": "3a14aa17-0d49-e3f6-2dd6-28c2e8194fbe", + "B07": "3a14aa17-0d49-11a6-b861-ee895121bf52", + "B08": "3a14aa17-0d49-9c7d-1145-d554a6e482f0", + "C05": "3a14aa17-0d49-45c4-7a34-5105bc3e2368", + "C06": "3a14aa17-0d49-867e-39ab-31b3fe9014be", + "C07": "3a14aa17-0d49-ec56-c4b4-39fd9b2131e7", + "C08": "3a14aa17-0d49-1128-d7d9-ffb1231c98c0", + "D05": "3a14aa17-0d49-e843-f961-ea173326a14b", + "D06": "3a14aa17-0d49-4d26-a985-f188359c4f8b", + "D07": "3a14aa17-0d49-223a-b520-bc092bb42fe0", + "D08": "3a14aa17-0d49-4fa3-401a-6a444e1cca22", + }, + }, + + # 站内试剂存放堆栈 + "站内试剂存放堆栈": { + "uuid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c", + "site_uuids": { + "A01": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5", + "A02": "3a14aa3b-9fab-ca72-febc-b7c304476c78" + } + }, + + + # 测量小瓶仓库(测密度) + "测量小瓶仓库": { + "uuid": "3a15012f-705b-c0de-3f9e-950c205f9921", + "site_uuids": { + "A01": "3a15012f-705e-0524-3161-c523b5aebc97", + "A02": "3a15012f-705e-7cd1-32ab-ad4fd1ab75c8", + "A03": "3a15012f-705e-a5d6-edac-bdbfec236260", + "B01": "3a15012f-705e-e0ee-80e0-10a6b3fc500d", + "B02": "3a15012f-705e-e499-180d-de06d60d0b21", + "B03": "3a15012f-705e-eff6-63f1-09f742096b26" + } + }, + + # 站内Tip盒堆栈 - 用于存放枪头盒 (耗材) + "站内Tip盒堆栈": { + "uuid": "3a14aa3a-2d3c-b5c1-9ddf-7c4a957d459a", + "site_uuids": { + "A01": "3a14aa3a-2d3d-e700-411a-0ddf85e1f18a", + "A02": "3a14aa3a-2d3d-a7ce-099a-d5632fdafa24", + "A03": "3a14aa3a-2d3d-bdf6-a702-c60b38b08501", + "B01": "3a14aa3a-2d3d-d704-f076-2a8d5bc72cb8", + "B02": "3a14aa3a-2d3d-c350-2526-0778d173a5ac", + "B03": "3a14aa3a-2d3d-bc38-b356-f0de2e44e0c7" + } + }, + # ========== 配液站仓库 ========== + "粉末堆栈": { + "uuid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0", + "site_uuids": { + "A01": "3a14198e-6929-31f0-8a22-0f98f72260df", + "A02": "3a14198e-6929-4379-affa-9a2935c17f99", + "A03": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af", + "A04": "3a14198e-6929-5e99-2b79-80720f7cfb54", + "B01": "3a14198e-6929-f525-9a1b-1857552b28ee", + "B02": "3a14198e-6929-bf98-0fd5-26e1d68bf62d", + "B03": "3a14198e-6929-2d86-a468-602175a2b5aa", + "B04": "3a14198e-6929-1a98-ae57-e97660c489ad", + "C01": "3a14198e-6929-46fe-841e-03dd753f1e4a", + "C02": "3a14198e-6929-72ac-32ce-9b50245682b8", + "C03": "3a14198e-6929-8a0b-b686-6f4a2955c4e2", + "C04": "3a14198e-6929-a0ec-5f15-c0f9f339f963", + "D01": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95", + "D02": "3a14198e-6929-3bd8-e6c7-4a9fd93be118", + "D03": "3a14198e-6929-dde1-fc78-34a84b71afdf", + "D04": "3a14198e-6929-7ac8-915a-fea51cb2e884" + } + }, + "溶液堆栈": { + "uuid": "3a14198e-d723-2c13-7d12-50143e190a23", + "site_uuids": { + "A01": "3a14198e-d724-e036-afdc-2ae39a7f3383", + "A02": "3a14198e-d724-d818-6d4f-5725191a24b5", + "A03": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31", + "A04": "3a14198e-d724-d378-d266-2508a224a19f", + "B01": "3a14198e-d724-afa4-fc82-0ac8a9016791", + "B02": "3a14198e-d724-be8a-5e0b-012675e195c6", + "B03": "3a14198e-d724-ab4e-48cb-817c3c146707", + "B04": "3a14198e-d724-f56e-468b-0110a8feb36a", + "C01": "3a14198e-d724-ca48-bb9e-7e85751e55b6", + "C02": "3a14198e-d724-cc1e-5c2c-228a130f40a8", + "C03": "3a14198e-d724-7f18-1853-39d0c62e1d33", + "C04": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c", + "D01": "3a14198e-d724-df6d-5e32-5483b3cab583", + "D02": "3a14198e-d724-1e28-c885-574c3df468d0", + "D03": "3a14198e-d724-28a2-a760-baa896f46b66", + "D04": "3a14198e-d724-0ddd-9654-f9352a421de9" + } + }, + "试剂堆栈": { + "uuid": "3a14198c-c2cc-0290-e086-44a428fba248", + "site_uuids": { + "A01": "3a14198c-c2cf-8b40-af28-b467808f1c36", # x=1, y=1, code=0001-0001 + "A02": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094", # x=1, y=2, code=0001-0002 + "A03": "3a14198c-c2d0-354f-39ad-642e1a72fcb8", # x=1, y=3, code=0001-0003 + "A04": "3a14198c-c2d0-725e-523d-34c037ac2440", # x=1, y=4, code=0001-0004 + "B01": "3a14198c-c2d0-f3e7-871a-e470d144296f", # x=2, y=1, code=0001-0005 + "B02": "3a14198c-c2d0-2070-efc8-44e245f10c6f", # x=2, y=2, code=0001-0006 + "B03": "3a14198c-c2d0-1559-105d-0ea30682cab4", # x=2, y=3, code=0001-0007 + "B04": "3a14198c-c2d0-efce-0939-69ca5a7dfd39" # x=2, y=4, code=0001-0008 + } + } +} + +# ============================================================================ +# 物料类型配置 +# ============================================================================ +# 说明: +# - 格式: PyLabRobot资源类型名称 → Bioyond系统typeId的UUID +# - 这个映射基于 resource.model 属性 (不是显示名称!) +# - UUID为空表示该类型暂未在Bioyond系统中定义 +MATERIAL_TYPE_MAPPINGS = { + # ================================================配液站资源============================================================ + # ==================================================样品=============================================================== + "BIOYOND_PolymerStation_1FlaskCarrier": ("烧杯", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), # 配液站-样品-烧杯 + "BIOYOND_PolymerStation_1BottleCarrier": ("试剂瓶", "3a14196b-8bcf-a460-4f74-23f21ca79e72"), # 配液站-样品-试剂瓶 + "BIOYOND_PolymerStation_6StockCarrier": ("分装板", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), # 配液站-样品-分装板 + "BIOYOND_PolymerStation_Liquid_Vial": ("10%分装小瓶", "3a14196c-76be-2279-4e22-7310d69aed68"), # 配液站-样品-分装板-第一排小瓶 + "BIOYOND_PolymerStation_Solid_Vial": ("90%分装小瓶", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), # 配液站-样品-分装板-第二排小瓶 + # ==================================================试剂=============================================================== + "BIOYOND_PolymerStation_8StockCarrier": ("样品板", "3a14196e-b7a0-a5da-1931-35f3000281e9"), # 配液站-试剂-样品板(8孔) + "BIOYOND_PolymerStation_Solid_Stock": ("样品瓶", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), # 配液站-试剂-样品板-样品瓶 + +} + +# ============================================================================ +# 动态生成的库位UUID映射(从WAREHOUSE_MAPPING中提取) +# ============================================================================ + +LOCATION_MAPPING = {} +for warehouse_name, warehouse_config in WAREHOUSE_MAPPING.items(): + if "site_uuids" in warehouse_config: + LOCATION_MAPPING.update(warehouse_config["site_uuids"]) + +# ============================================================================ +# 物料默认参数配置 +# ============================================================================ +# 说明: +# - 为特定物料名称自动添加默认参数(如密度、分子量、单位等) +# - 格式: 物料名称 → {参数字典} +# - 在创建或更新物料时,会自动合并这些参数到 Parameters 字段 +# - unit: 物料的计量单位(会用于 unit 字段) +# - density/densityUnit: 密度信息(会添加到 Parameters 中) + +MATERIAL_DEFAULT_PARAMETERS = { + # 溶剂类 + "NMP": { + "unit": "毫升", + "density": "1.03", + "densityUnit": "g/mL", + "description": "N-甲基吡咯烷酮 (N-Methyl-2-pyrrolidone)" + }, + # 可以继续添加其他物料... +} + +# ============================================================================ +# 物料类型默认参数配置 +# ============================================================================ +# 说明: +# - 为特定物料类型(UUID)自动添加默认参数 +# - 格式: Bioyond类型UUID → {参数字典} +# - 优先级低于按名称匹配的配置 +MATERIAL_TYPE_PARAMETERS = { + # 示例: + # "3a14196b-24f2-ca49-9081-0cab8021bf1a": { # 烧杯 + # "unit": "个" + # } +} diff --git a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py b/unilabos/devices/workstation/bioyond_studio/dispensing_station/dispensing_station.py similarity index 79% rename from unilabos/devices/workstation/bioyond_studio/dispensing_station.py rename to unilabos/devices/workstation/bioyond_studio/dispensing_station/dispensing_station.py index 6d512720..dc48487b 100644 --- a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py +++ b/unilabos/devices/workstation/bioyond_studio/dispensing_station/dispensing_station.py @@ -4,7 +4,8 @@ from typing import Optional, Dict, Any, List from typing_extensions import TypedDict import requests -from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG +import pint + from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation @@ -25,13 +26,89 @@ class ComputeExperimentDesignReturn(TypedDict): class BioyondDispensingStation(BioyondWorkstation): def __init__( self, - config, - # 桌子 - deck, - *args, + config: dict = None, + deck=None, + protocol_type=None, **kwargs, - ): - super().__init__(config, deck, *args, **kwargs) + ): + """初始化配液站 + + Args: + config: 配置字典,应包含material_type_mappings等配置 + deck: Deck对象 + protocol_type: 协议类型(由ROS系统传递,此处忽略) + **kwargs: 其他可能的参数 + """ + if config is None: + config = {} + + # 将 kwargs 合并到 config 中 (处理扁平化配置如 api_key) + config.update(kwargs) + + if deck is None and config: + deck = config.get('deck') + + # 🔧 修复: 确保 Deck 上的 warehouses 具有正确的 UUID (必须在 super().__init__ 之前执行,因为父类会触发同步) + # 从配置中读取 warehouse_mapping,并应用到实际的 deck 资源上 + if config and "warehouse_mapping" in config and deck: + warehouse_mapping = config["warehouse_mapping"] + print(f"正在根据配置更新 Deck warehouse UUIDs... (共有 {len(warehouse_mapping)} 个配置)") + + user_deck = deck + # 初始化 warehouses 字典 + if not hasattr(user_deck, "warehouses") or user_deck.warehouses is None: + user_deck.warehouses = {} + + # 1. 尝试从 children 中查找匹配的资源 + for child in user_deck.children: + # 简单判断: 如果名字在 mapping 中,就认为是 warehouse + if child.name in warehouse_mapping: + user_deck.warehouses[child.name] = child + print(f" - 从子资源中找到 warehouse: {child.name}") + + # 2. 如果还是没找到,且 Deck 类有 setup 方法,尝试调用 setup (针对 Deck 对象正确但未初始化的情况) + if not user_deck.warehouses and hasattr(user_deck, "setup"): + print(" - 尝试调用 deck.setup() 初始化仓库...") + try: + user_deck.setup() + # setup 后重新检查 + if hasattr(user_deck, "warehouses") and user_deck.warehouses: + print(f" - setup() 成功,找到 {len(user_deck.warehouses)} 个仓库") + except Exception as e: + print(f" - 调用 setup() 失败: {e}") + + # 3. 如果仍然为空,可能需要手动创建 (仅针对特定已知的 Deck 类型进行补救,这里暂时只打印警告) + if not user_deck.warehouses: + print(" - ⚠️ 仍然无法找到任何 warehouse 资源!") + + for wh_name, wh_config in warehouse_mapping.items(): + target_uuid = wh_config.get("uuid") + + # 尝试在 deck.warehouses 中查找 + wh_resource = None + if hasattr(user_deck, "warehouses") and wh_name in user_deck.warehouses: + wh_resource = user_deck.warehouses[wh_name] + + # 如果没找到,尝试在所有子资源中查找 + if not wh_resource: + wh_resource = user_deck.get_resource(wh_name) + + if wh_resource: + if target_uuid: + current_uuid = getattr(wh_resource, "uuid", None) + print(f"✅ 更新仓库 '{wh_name}' UUID: {current_uuid} -> {target_uuid}") + + # 动态添加 uuid 属性 + wh_resource.uuid = target_uuid + # 同时也确保 category 正确,避免 graphio 识别错误 + # wh_resource.category = "warehouse" + else: + print(f"⚠️ 仓库 '{wh_name}' 在配置中没有 UUID") + else: + print(f"❌ 在 Deck 中未找到配置的仓库: '{wh_name}'") + + super().__init__(bioyond_config=config, deck=deck) + # self.config = config # self.api_key = config["api_key"] # self.host = config["api_host"] @@ -43,6 +120,41 @@ def __init__( # 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}} self.order_completion_status = {} + # 初始化 pint 单位注册表 + self.ureg = pint.UnitRegistry() + + # 化合物信息 + self.compound_info = { + "MolWt": { + "MDA": 108.14 * self.ureg.g / self.ureg.mol, + "TDA": 122.16 * self.ureg.g / self.ureg.mol, + "PAPP": 521.62 * self.ureg.g / self.ureg.mol, + "BTDA": 322.23 * self.ureg.g / self.ureg.mol, + "BPDA": 294.22 * self.ureg.g / self.ureg.mol, + "6FAP": 366.26 * self.ureg.g / self.ureg.mol, + "PMDA": 218.12 * self.ureg.g / self.ureg.mol, + "MPDA": 108.14 * self.ureg.g / self.ureg.mol, + "SIDA": 248.51 * self.ureg.g / self.ureg.mol, + "ODA": 200.236 * self.ureg.g / self.ureg.mol, + "4,4'-ODA": 200.236 * self.ureg.g / self.ureg.mol, + "134": 292.34 * self.ureg.g / self.ureg.mol, + }, + "FuncGroup": { + "MDA": "Amine", + "TDA": "Amine", + "PAPP": "Amine", + "BTDA": "Anhydride", + "BPDA": "Anhydride", + "6FAP": "Amine", + "MPDA": "Amine", + "SIDA": "Amine", + "PMDA": "Anhydride", + "ODA": "Amine", + "4,4'-ODA": "Amine", + "134": "Amine", + } + } + def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]: """项目接口通用POST调用 @@ -54,7 +166,7 @@ def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]: dict: 服务端响应,失败时返回 {code:0,message,...} """ request_data = { - "apiKey": API_CONFIG["api_key"], + "apiKey": self.bioyond_config["api_key"], "requestTime": self.hardware_interface.get_current_time_iso8601(), "data": data } @@ -85,7 +197,7 @@ def _delete_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]: dict: 服务端响应,失败时返回 {code:0,message,...} """ request_data = { - "apiKey": API_CONFIG["api_key"], + "apiKey": self.bioyond_config["api_key"], "requestTime": self.hardware_interface.get_current_time_iso8601(), "data": data } @@ -118,20 +230,22 @@ def compute_experiment_design( ratio = json.loads(ratio) except Exception: ratio = {} - root = str(Path(__file__).resolve().parents[3]) - if root not in sys.path: - sys.path.append(root) - try: - mod = importlib.import_module("tem.compute") - except Exception as e: - raise BioyondException(f"无法导入计算模块: {e}") try: wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent mt = float(m_tot) if isinstance(m_tot, str) else m_tot tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent except Exception as e: raise BioyondException(f"参数解析失败: {e}") - res = mod.generate_experiment_design(ratio=ratio, wt_percent=wp, m_tot=mt, titration_percent=tp) + + # 2. 调用内部计算方法 + res = self._generate_experiment_design( + ratio=ratio, + wt_percent=wp, + m_tot=mt, + titration_percent=tp + ) + + # 3. 构造返回结果 out = { "solutions": res.get("solutions", []), "titration": res.get("titration", {}), @@ -140,11 +254,248 @@ def compute_experiment_design( "return_info": json.dumps(res, ensure_ascii=False) } return out + except BioyondException: raise except Exception as e: raise BioyondException(str(e)) + def _generate_experiment_design( + self, + ratio: dict, + wt_percent: float = 0.25, + m_tot: float = 70, + titration_percent: float = 0.03, + ) -> dict: + """内部方法:生成实验设计 + + 根据FuncGroup自动区分二胺和二酐,每种二胺单独配溶液,严格按照ratio顺序投料。 + + 参数: + ratio: 化合物配比字典,格式: {"compound_name": ratio_value} + wt_percent: 固体重量百分比 + m_tot: 反应混合物总质量(g) + titration_percent: 滴定溶液百分比 + + 返回: + 包含实验设计详细参数的字典 + """ + # 溶剂密度 + ρ_solvent = 1.03 * self.ureg.g / self.ureg.ml + # 二酐溶解度 + solubility = 0.02 * self.ureg.g / self.ureg.ml + # 投入固体时最小溶剂体积 + V_min = 30 * self.ureg.ml + m_tot = m_tot * self.ureg.g + + # 保持ratio中的顺序 + compound_names = list(ratio.keys()) + compound_ratios = list(ratio.values()) + + # 验证所有化合物是否在 compound_info 中定义 + undefined_compounds = [name for name in compound_names if name not in self.compound_info["MolWt"]] + if undefined_compounds: + available = list(self.compound_info["MolWt"].keys()) + raise ValueError( + f"以下化合物未在 compound_info 中定义: {undefined_compounds}。" + f"可用的化合物: {available}" + ) + + # 获取各化合物的分子量和官能团类型 + molecular_weights = [self.compound_info["MolWt"][name] for name in compound_names] + func_groups = [self.compound_info["FuncGroup"][name] for name in compound_names] + + # 记录化合物信息用于调试 + self.hardware_interface._logger.info(f"化合物名称: {compound_names}") + self.hardware_interface._logger.info(f"官能团类型: {func_groups}") + + # 按原始顺序分离二胺和二酐 + ordered_compounds = list(zip(compound_names, compound_ratios, molecular_weights, func_groups)) + diamine_compounds = [(name, ratio_val, mw, i) for i, (name, ratio_val, mw, fg) in enumerate(ordered_compounds) if fg == "Amine"] + anhydride_compounds = [(name, ratio_val, mw, i) for i, (name, ratio_val, mw, fg) in enumerate(ordered_compounds) if fg == "Anhydride"] + + if not diamine_compounds or not anhydride_compounds: + raise ValueError( + f"需要同时包含二胺(Amine)和二酐(Anhydride)化合物。" + f"当前二胺: {[c[0] for c in diamine_compounds]}, " + f"当前二酐: {[c[0] for c in anhydride_compounds]}" + ) + + # 计算加权平均分子量 (基于摩尔比) + total_molar_ratio = sum(compound_ratios) + weighted_molecular_weight = sum(ratio_val * mw for ratio_val, mw in zip(compound_ratios, molecular_weights)) + + # 取最后一个二酐用于滴定 + titration_anhydride = anhydride_compounds[-1] + solid_anhydrides = anhydride_compounds[:-1] if len(anhydride_compounds) > 1 else [] + + # 二胺溶液配制参数 - 每种二胺单独配制 + diamine_solutions = [] + total_diamine_volume = 0 * self.ureg.ml + + # 计算反应物的总摩尔量 + n_reactant = m_tot * wt_percent / weighted_molecular_weight + + for name, ratio_val, mw, order_index in diamine_compounds: + # 跳过 SIDA + if name == "SIDA": + continue + + # 计算该二胺需要的摩尔数 + n_diamine_needed = n_reactant * ratio_val + + # 二胺溶液配制参数 (每种二胺固定配制参数) + m_diamine_solid = 5.0 * self.ureg.g # 每种二胺固体质量 + V_solvent_for_this = 20 * self.ureg.ml # 每种二胺溶剂体积 + m_solvent_for_this = ρ_solvent * V_solvent_for_this + + # 计算该二胺溶液的浓度 + c_diamine = (m_diamine_solid / mw) / V_solvent_for_this + + # 计算需要移取的溶液体积 + V_diamine_needed = n_diamine_needed / c_diamine + + diamine_solutions.append({ + "name": name, + "order": order_index, + "solid_mass": m_diamine_solid.magnitude, + "solvent_volume": V_solvent_for_this.magnitude, + "concentration": c_diamine.magnitude, + "volume_needed": V_diamine_needed.magnitude, + "molar_ratio": ratio_val + }) + + total_diamine_volume += V_diamine_needed + + # 按原始顺序排序 + diamine_solutions.sort(key=lambda x: x["order"]) + + # 计算滴定二酐的质量 + titration_name, titration_ratio, titration_mw, _ = titration_anhydride + m_titration_anhydride = n_reactant * titration_ratio * titration_mw + m_titration_90 = m_titration_anhydride * (1 - titration_percent) + m_titration_10 = m_titration_anhydride * titration_percent + + # 计算其他固体二酐的质量 (按顺序) + solid_anhydride_masses = [] + for name, ratio_val, mw, order_index in solid_anhydrides: + mass = n_reactant * ratio_val * mw + solid_anhydride_masses.append({ + "name": name, + "order": order_index, + "mass": mass.magnitude, + "molar_ratio": ratio_val + }) + + # 按原始顺序排序 + solid_anhydride_masses.sort(key=lambda x: x["order"]) + + # 计算溶剂用量 + total_diamine_solution_mass = sum( + sol["volume_needed"] * ρ_solvent for sol in diamine_solutions + ) * self.ureg.ml + + # 预估滴定溶剂量、计算补加溶剂量 + m_solvent_titration = m_titration_10 / solubility * ρ_solvent + m_solvent_add = m_tot * (1 - wt_percent) - total_diamine_solution_mass - m_solvent_titration + + # 检查最小溶剂体积要求 + total_liquid_volume = (total_diamine_solution_mass + m_solvent_add) / ρ_solvent + m_tot_min = V_min / total_liquid_volume * m_tot + + # 如果需要,按比例放大 + scale_factor = 1.0 + if m_tot_min > m_tot: + scale_factor = (m_tot_min / m_tot).magnitude + m_titration_90 *= scale_factor + m_titration_10 *= scale_factor + m_solvent_add *= scale_factor + m_solvent_titration *= scale_factor + + # 更新二胺溶液用量 + for sol in diamine_solutions: + sol["volume_needed"] *= scale_factor + + # 更新固体二酐用量 + for anhydride in solid_anhydride_masses: + anhydride["mass"] *= scale_factor + + m_tot = m_tot_min + + # 生成投料顺序 + feeding_order = [] + + # 1. 固体二酐 (按顺序) + for anhydride in solid_anhydride_masses: + feeding_order.append({ + "step": len(feeding_order) + 1, + "type": "solid_anhydride", + "name": anhydride["name"], + "amount": anhydride["mass"], + "order": anhydride["order"] + }) + + # 2. 二胺溶液 (按顺序) + for sol in diamine_solutions: + feeding_order.append({ + "step": len(feeding_order) + 1, + "type": "diamine_solution", + "name": sol["name"], + "amount": sol["volume_needed"], + "order": sol["order"] + }) + + # 3. 主要二酐粉末 + feeding_order.append({ + "step": len(feeding_order) + 1, + "type": "main_anhydride", + "name": titration_name, + "amount": m_titration_90.magnitude, + "order": titration_anhydride[3] + }) + + # 4. 补加溶剂 + if m_solvent_add > 0: + feeding_order.append({ + "step": len(feeding_order) + 1, + "type": "additional_solvent", + "name": "溶剂", + "amount": m_solvent_add.magnitude, + "order": 999 + }) + + # 5. 滴定二酐溶液 + feeding_order.append({ + "step": len(feeding_order) + 1, + "type": "titration_anhydride", + "name": f"{titration_name} 滴定液", + "amount": m_titration_10.magnitude, + "titration_solvent": m_solvent_titration.magnitude, + "order": titration_anhydride[3] + }) + + # 返回实验设计结果 + results = { + "total_mass": m_tot.magnitude, + "scale_factor": scale_factor, + "solutions": diamine_solutions, + "solids": solid_anhydride_masses, + "titration": { + "name": titration_name, + "main_portion": m_titration_90.magnitude, + "titration_portion": m_titration_10.magnitude, + "titration_solvent": m_solvent_titration.magnitude, + }, + "solvents": { + "additional_solvent": m_solvent_add.magnitude, + "total_liquid_volume": total_liquid_volume.magnitude + }, + "feeding_order": feeding_order, + "minimum_required_mass": m_tot_min.magnitude + } + + return results + # 90%10%小瓶投料任务创建方法 def create_90_10_vial_feeding_task(self, order_name: str = None, @@ -961,6 +1312,108 @@ def _extract_actuals_from_report(self, report) -> Dict[str, Any]: 'actualVolume': actual_volume } + def _simplify_report(self, report) -> Dict[str, Any]: + """简化实验报告,只保留关键信息,去除冗余的工作流参数""" + if not isinstance(report, dict): + return report + + data = report.get('data', {}) + if not isinstance(data, dict): + return report + + # 提取关键信息 + simplified = { + 'name': data.get('name'), + 'code': data.get('code'), + 'requester': data.get('requester'), + 'workflowName': data.get('workflowName'), + 'workflowStep': data.get('workflowStep'), + 'requestTime': data.get('requestTime'), + 'startPreparationTime': data.get('startPreparationTime'), + 'completeTime': data.get('completeTime'), + 'useTime': data.get('useTime'), + 'status': data.get('status'), + 'statusName': data.get('statusName'), + } + + # 提取物料信息(简化版) + pre_intakes = data.get('preIntakes', []) + if pre_intakes and isinstance(pre_intakes, list): + first_intake = pre_intakes[0] + sample_materials = first_intake.get('sampleMaterials', []) + + # 简化物料信息 + simplified_materials = [] + for material in sample_materials: + if isinstance(material, dict): + mat_info = { + 'materialName': material.get('materialName'), + 'materialTypeName': material.get('materialTypeName'), + 'materialCode': material.get('materialCode'), + 'materialLocation': material.get('materialLocation'), + } + + # 解析parameters中的关键信息(如密度、加料历史等) + params_str = material.get('parameters', '{}') + try: + params = json.loads(params_str) if isinstance(params_str, str) else params_str + if isinstance(params, dict): + # 只保留关键参数 + if 'density' in params: + mat_info['density'] = params['density'] + if 'feedingHistory' in params: + mat_info['feedingHistory'] = params['feedingHistory'] + if 'liquidVolume' in params: + mat_info['liquidVolume'] = params['liquidVolume'] + if 'm_diamine_tot' in params: + mat_info['m_diamine_tot'] = params['m_diamine_tot'] + if 'wt_diamine' in params: + mat_info['wt_diamine'] = params['wt_diamine'] + except: + pass + + simplified_materials.append(mat_info) + + simplified['sampleMaterials'] = simplified_materials + + # 提取extraProperties中的实际值 + extra_props = first_intake.get('extraProperties', {}) + if isinstance(extra_props, dict): + simplified_extra = {} + for key, value in extra_props.items(): + try: + parsed_value = json.loads(value) if isinstance(value, str) else value + simplified_extra[key] = parsed_value + except: + simplified_extra[key] = value + simplified['extraProperties'] = simplified_extra + + return { + 'data': simplified, + 'code': report.get('code'), + 'message': report.get('message'), + 'timestamp': report.get('timestamp') + } + + def scheduler_start(self) -> dict: + """启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务 + + Returns: + dict: 包含return_info的字典,return_info为整型(1=成功) + + Raises: + BioyondException: 调度器启动失败时抛出异常 + """ + result = self.hardware_interface.scheduler_start() + self.hardware_interface._logger.info(f"调度器启动结果: {result}") + + if result != 1: + error_msg = "启动调度器失败: 有未处理错误,调度无法启动。请检查Bioyond系统状态。" + self.hardware_interface._logger.error(error_msg) + raise BioyondException(error_msg) + + return {"return_info": result} + # 等待多个任务完成并获取实验报告 def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = None, @@ -1002,7 +1455,12 @@ def wait_for_multiple_orders_and_get_reports(self, # 验证batch_create_result参数 if not batch_create_result or batch_create_result == "": - raise BioyondException("batch_create_result参数为空,请确保从batch_create节点正确连接handle") + raise BioyondException( + "batch_create_result参数为空,请确保:\n" + "1. batch_create节点与wait节点之间正确连接了handle\n" + "2. batch_create节点成功执行并返回了结果\n" + "3. 检查上游batch_create任务是否成功创建了订单" + ) # 解析batch_create_result JSON对象 try: @@ -1031,7 +1489,17 @@ def wait_for_multiple_orders_and_get_reports(self, # 验证提取的数据 if not order_codes: - raise BioyondException("batch_create_result中未找到order_codes字段或为空") + self.hardware_interface._logger.error( + f"batch_create任务未生成任何订单。batch_create_result内容: {batch_create_result}" + ) + raise BioyondException( + "batch_create_result中未找到order_codes或为空。\n" + "可能的原因:\n" + "1. batch_create任务执行失败(检查任务是否报错)\n" + "2. 物料配置问题(如'物料样品板分配失败')\n" + "3. Bioyond系统状态异常\n" + f"请检查batch_create任务的执行结果" + ) if not order_ids: raise BioyondException("batch_create_result中未找到order_ids字段或为空") @@ -1114,6 +1582,8 @@ def wait_for_multiple_orders_and_get_reports(self, self.hardware_interface._logger.info( f"成功获取任务 {order_code} 的实验报告" ) + # 简化报告,去除冗余信息 + report = self._simplify_report(report) reports.append({ "order_code": order_code, @@ -1288,7 +1758,7 @@ def transfer_materials_to_reaction_station( f"开始执行批量物料转移: {len(transfer_groups)}组任务 -> {target_device_id}" ) - from .config import WAREHOUSE_MAPPING + warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) results = [] successful_count = 0 failed_count = 0 diff --git a/unilabos/devices/workstation/bioyond_studio/reaction_station.py b/unilabos/devices/workstation/bioyond_studio/reaction_station/reaction_station.py similarity index 55% rename from unilabos/devices/workstation/bioyond_studio/reaction_station.py rename to unilabos/devices/workstation/bioyond_studio/reaction_station/reaction_station.py index ffb83fd3..c7f3194a 100644 --- a/unilabos/devices/workstation/bioyond_studio/reaction_station.py +++ b/unilabos/devices/workstation/bioyond_studio/reaction_station/reaction_station.py @@ -2,17 +2,15 @@ import time import requests from typing import List, Dict, Any +import json +import requests from pathlib import Path from datetime import datetime from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import MachineState from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String -from unilabos.devices.workstation.bioyond_studio.config import ( - WORKFLOW_STEP_IDS, - WORKFLOW_TO_SECTION_MAP, - ACTION_NAMES -) -from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG + + class BioyondReactor: @@ -49,21 +47,84 @@ def _f(v): class BioyondReactionStation(BioyondWorkstation): """Bioyond反应站类 - 继承自BioyondWorkstation,提供反应站特定的业务方法 + 继承自BioyondWorkstation,提供反应站特定的业务方法 """ def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs): """初始化反应站 Args: - config: 配置字典,应包含workflow_mappings等配置 + config: 配置字典,应包含workflow_mappings等配置 deck: Deck对象 - protocol_type: 协议类型(由ROS系统传递,此处忽略) + protocol_type: 协议类型(由ROS系统传递,此处忽略) **kwargs: 其他可能的参数 """ + if config is None: + config = {} + + # 将 kwargs 合并到 config 中 (处理扁平化配置如 api_key) + config.update(kwargs) + if deck is None and config: deck = config.get('deck') + # 🔧 修复: 确保 Deck 上的 warehouses 具有正确的 UUID (必须在 super().__init__ 之前执行,因为父类会触发同步) + # 从配置中读取 warehouse_mapping,并应用到实际的 deck 资源上 + if config and "warehouse_mapping" in config and deck: + warehouse_mapping = config["warehouse_mapping"] + print(f"正在根据配置更新 Deck warehouse UUIDs... (共有 {len(warehouse_mapping)} 个配置)") + + user_deck = deck + # 初始化 warehouses 字典 + if not hasattr(user_deck, "warehouses") or user_deck.warehouses is None: + user_deck.warehouses = {} + + # 1. 尝试从 children 中查找匹配的资源 + for child in user_deck.children: + # 简单判断: 如果名字在 mapping 中,就认为是 warehouse + if child.name in warehouse_mapping: + user_deck.warehouses[child.name] = child + print(f" - 从子资源中找到 warehouse: {child.name}") + + # 2. 如果还是没找到,且 Deck 类有 setup 方法,尝试调用 setup (针对 Deck 对象正确但未初始化的情况) + if not user_deck.warehouses and hasattr(user_deck, "setup"): + print(" - 尝试调用 deck.setup() 初始化仓库...") + try: + user_deck.setup() + # setup 后重新检查 + if hasattr(user_deck, "warehouses") and user_deck.warehouses: + print(f" - setup() 成功,找到 {len(user_deck.warehouses)} 个仓库") + except Exception as e: + print(f" - 调用 setup() 失败: {e}") + + # 3. 如果仍然为空,可能需要手动创建 (仅针对特定已知的 Deck 类型进行补救,这里暂时只打印警告) + if not user_deck.warehouses: + print(" - ⚠️ 仍然无法找到任何 warehouse 资源!") + + for wh_name, wh_config in warehouse_mapping.items(): + target_uuid = wh_config.get("uuid") + + # 尝试在 deck.warehouses 中查找 + wh_resource = None + if hasattr(user_deck, "warehouses") and wh_name in user_deck.warehouses: + wh_resource = user_deck.warehouses[wh_name] + + # 如果没找到,尝试在所有子资源中查找 + if not wh_resource: + wh_resource = user_deck.get_resource(wh_name) + + if wh_resource: + if target_uuid: + current_uuid = getattr(wh_resource, "uuid", None) + print(f"✅ 更新仓库 '{wh_name}' UUID: {current_uuid} -> {target_uuid}") + wh_resource.uuid = target_uuid + else: + print(f"⚠️ 仓库 '{wh_name}' 在配置中没有 UUID") + else: + print(f"❌ 在 Deck 中未找到配置的仓库: '{wh_name}'") + + super().__init__(bioyond_config=config, deck=deck) + print(f"BioyondReactionStation初始化 - config包含workflow_mappings: {'workflow_mappings' in (config or {})}") if config and 'workflow_mappings' in config: print(f"workflow_mappings内容: {config['workflow_mappings']}") @@ -86,6 +147,147 @@ def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs) self._frame_to_reactor_id = {1: "reactor_1", 2: "reactor_2", 3: "reactor_3", 4: "reactor_4", 5: "reactor_5"} + # 用于缓存从 Bioyond 查询的工作流序列 + self._cached_workflow_sequence = [] + # 用于缓存待处理的时间约束 + self.pending_time_constraints = [] + + # 从配置中获取 action_names + self.action_names = self.bioyond_config.get("action_names", {}) + + # 动态获取工作流步骤ID + self.workflow_step_ids = self._fetch_workflow_step_ids() + + def _fetch_workflow_step_ids(self) -> Dict[str, Dict[str, str]]: + """动态获取工作流步骤ID""" + print("正在从LIMS获取最新工作流步骤ID...") + + api_host = self.bioyond_config.get("api_host") + api_key = self.bioyond_config.get("api_key") + + if not api_host or not api_key: + print("API配置缺失,无法动态获取工作流步骤ID") + return {} + + def call_api(endpoint, data=None): + url = f"{api_host}{endpoint}" + payload = { + "apiKey": api_key, + "requestTime": datetime.now().isoformat(), + "data": data if data else {} + } + try: + response = requests.post(url, json=payload, headers={"Content-Type": "application/json"}, timeout=5) + return response.json() + except Exception as e: + print(f"调用API {endpoint} 失败: {e}") + return None + + # 1. 获取工作流列表 + resp = call_api("/api/lims/workflow/work-flow-list", {"type": 2, "includeDetail": True}) + if not resp: + print("无法获取工作流列表") + return {} + + workflows = resp.get("data", []) + if isinstance(workflows, dict): + if "list" in workflows: + workflows = workflows["list"] + elif "items" in workflows: + workflows = workflows["items"] + + if not workflows: + print("工作流列表为空") + return {} + + new_ids = {} + + #从配置中获取workflow_to_section_map + workflow_to_section_map = self.bioyond_config.get("workflow_to_section_map", {}) + + # 2. 遍历映射表 + for internal_name, section_name in workflow_to_section_map.items(): + # 查找对应的工作流对象 + wf_obj = next((w for w in workflows if w.get("name") == section_name), None) + if not wf_obj: + # print(f"未找到工作流: {section_name}") + continue + + # 获取 subWorkflowId + sub_wf_id = None + if wf_obj.get("subWorkflows"): + sub_wfs = wf_obj.get("subWorkflows") + if len(sub_wfs) > 0: + sub_wf_id = sub_wfs[0].get("id") + + if not sub_wf_id: + # print(f"工作流 {section_name} 没有子工作流ID") + continue + + # 3. 获取步骤参数 + step_resp = call_api("/api/lims/workflow/sub-workflow-step-parameters", sub_wf_id) + if not step_resp or not step_resp.get("data"): + # print(f"无法获取工作流 {section_name} 的步骤参数") + continue + + steps_data = step_resp.get("data", {}) + step_name_to_id = {} + + if isinstance(steps_data, dict): + for s_id, step_list in steps_data.items(): + if isinstance(step_list, list): + for step in step_list: + s_name = step.get("name") + if s_name: + step_name_to_id[s_name] = s_id + + # 4. 匹配 ACTION_NAMES + target_key = internal_name + normalized_key = internal_name.lower().replace('(', '_').replace(')', '').replace('-', '_') + + if internal_name in self.action_names: + target_key = internal_name + elif normalized_key in self.action_names: + target_key = normalized_key + elif internal_name.lower() in self.action_names: + target_key = internal_name.lower() + + if target_key in self.action_names: + new_ids[target_key] = {} + for key, action_display_name in self.action_names[target_key].items(): + step_id = step_name_to_id.get(action_display_name) + if step_id: + new_ids[target_key][key] = step_id + else: + print(f"警告: 工作流 '{section_name}' 中未找到步骤 '{action_display_name}'") + + if not new_ids: + print("未能获取任何新的步骤ID,使用默认配置") + return self.bioyond_config.get("workflow_step_ids", {}) + + print("成功更新工作流步骤ID") + return new_ids + + + @property + def workflow_sequence(self) -> str: + """工作流序列属性 - 返回初始化时查询的工作流列表 + + Returns: + str: 工作流信息的 JSON 字符串 + """ + import json + return json.dumps(self._cached_workflow_sequence, ensure_ascii=False) + + @workflow_sequence.setter + def workflow_sequence(self, value: List[str]): + """设置工作流序列 + + Args: + value: 工作流 ID 列表 + """ + self._cached_workflow_sequence = value + # ==================== 工作流方法 ==================== def reactor_taken_out(self): @@ -97,6 +299,27 @@ def reactor_taken_out(self): print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) + def scheduler_start(self) -> dict: + """启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务 + + Returns: + dict: 包含return_info的字典,return_info为整型(1=成功) + + Raises: + BioyondException: 调度器启动失败时抛出异常 + """ + from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException + + result = self.hardware_interface.scheduler_start() + self.hardware_interface._logger.info(f"调度器启动结果: {result}") + + if result != 1: + error_msg = "启动调度器失败: 有未处理错误,调度无法启动。请检查Bioyond系统状态。" + self.hardware_interface._logger.error(error_msg) + raise BioyondException(error_msg) + + return {"return_info": result} + def reactor_taken_in( self, assign_material_name: str, @@ -106,12 +329,12 @@ def reactor_taken_in( """反应器放入 Args: - assign_material_name: 物料名称(不能为空) - cutoff: 粘度上限(需为有效数字字符串,默认 "900000") - temperature: 温度设定(°C,范围:-50.00 至 100.00) + assign_material_name: 物料名称(不能为空) + cutoff: 粘度上限(需为有效数字字符串,默认 "900000") + temperature: 温度设定(C,范围:-50.00 至 100.00) Returns: - str: JSON 字符串,格式为 {"suc": True} + str: JSON 字符串,格式为 {"suc": True} Raises: ValueError: 若物料名称无效或 cutoff 格式错误 @@ -131,15 +354,16 @@ def reactor_taken_in( if isinstance(temperature, str): temperature = float(temperature) - step_id = WORKFLOW_STEP_IDS["reactor_taken_in"]["config"] + + step_id = self.workflow_step_ids["reactor_taken_in"]["config"] reactor_taken_in_params = { "param_values": { step_id: { - ACTION_NAMES["reactor_taken_in"]["config"]: [ + self.action_names["reactor_taken_in"]["config"]: [ {"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff}, {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} ], - ACTION_NAMES["reactor_taken_in"]["stirring"]: [ + self.action_names["reactor_taken_in"]["stirring"]: [ {"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"} ] } @@ -162,33 +386,40 @@ def solid_feeding_vials( """固体进料小瓶 Args: - material_id: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟) + material_id: 粉末类型ID, Salt=1, Flour=2, BTDA=3 time: 观察时间(分钟) - torque_variation: 是否观察(int类型, 1=否, 2=是) + torque_variation: 是否观察(NO=1, YES=2) assign_material_name: 物料名称(用于获取试剂瓶位ID) - temperature: 温度设定(°C) + temperature: 温度设定(C) """ + # 参数映射 + material_map = {"Salt": "1", "Flour": "2", "BTDA": "3", "1": "1", "2": "2", "3": "3"} + torque_map = {"NO": "1", "YES": "2", 1: "1", 2: "2", "1": "1", "2": "2"} + + mapped_material_id = material_map.get(str(material_id), str(material_id)) + mapped_torque_variation = int(torque_map.get(str(torque_variation), "1")) + self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None if isinstance(temperature, str): temperature = float(temperature) - feeding_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] - observe_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] + feeding_step_id = self.workflow_step_ids["solid_feeding_vials"]["feeding"] + observe_step_id = self.workflow_step_ids["solid_feeding_vials"]["observe"] solid_feeding_vials_params = { "param_values": { feeding_step_id: { - ACTION_NAMES["solid_feeding_vials"]["feeding"]: [ - {"m": 0, "n": 3, "Key": "materialId", "Value": material_id}, + self.action_names["solid_feeding_vials"]["feeding"]: [ + {"m": 0, "n": 3, "Key": "materialId", "Value": mapped_material_id}, {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m} if material_id_m else {} ] }, observe_step_id: { - ACTION_NAMES["solid_feeding_vials"]["observe"]: [ + self.action_names["solid_feeding_vials"]["observe"]: [ {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(mapped_torque_variation)}, {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} ] } @@ -196,7 +427,7 @@ def solid_feeding_vials( } self.pending_task_params.append(solid_feeding_vials_params) - print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, torque={torque_variation}, temp={temperature:.2f}°C") + print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, torque={torque_variation}, temp={temperature:.2f}C") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) @@ -214,11 +445,18 @@ def liquid_feeding_vials_non_titration( Args: volume_formula: 分液公式(μL) assign_material_name: 物料名称 - titration_type: 是否滴定(1=否, 2=是) + titration_type: 是否滴定(NO=1, YES=2) time: 观察时间(分钟) - torque_variation: 是否观察(int类型, 1=否, 2=是) - temperature: 温度(°C) + torque_variation: 是否观察(NO=1, YES=2) + temperature: 温度(C) """ + # 参数映射 + titration_map = {"NO": "1", "YES": "2", "1": "1", "2": "2"} + torque_map = {"NO": "1", "YES": "2", 1: "1", 2: "2", "1": "1", "2": "2"} + + mapped_titration_type = titration_map.get(str(titration_type), "1") + mapped_torque_variation = int(torque_map.get(str(torque_variation), "1")) + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if material_id is None: @@ -227,22 +465,22 @@ def liquid_feeding_vials_non_titration( if isinstance(temperature, str): temperature = float(temperature) - liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] - observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] + liquid_step_id = self.workflow_step_ids["liquid_feeding_vials_non_titration"]["liquid"] + observe_step_id = self.workflow_step_ids["liquid_feeding_vials_non_titration"]["observe"] params = { "param_values": { liquid_step_id: { - ACTION_NAMES["liquid_feeding_vials_non_titration"]["liquid"]: [ + self.action_names["liquid_feeding_vials_non_titration"]["liquid"]: [ {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}, - {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type} + {"m": 0, "n": 3, "Key": "titrationType", "Value": mapped_titration_type} ] }, observe_step_id: { - ACTION_NAMES["liquid_feeding_vials_non_titration"]["observe"]: [ + self.action_names["liquid_feeding_vials_non_titration"]["observe"]: [ {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(mapped_torque_variation)}, {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} ] } @@ -275,11 +513,18 @@ def liquid_feeding_solvents( "total_liquid_volume": 48.00916988195499 } 如果提供solvents,则从中提取additional_solvent并转换为μL - titration_type: 是否滴定(1=否, 2=是) + titration_type: 是否滴定(NO=1, YES=2) time: 观察时间(分钟) - torque_variation: 是否观察(int类型, 1=否, 2=是) - temperature: 温度设定(°C) + torque_variation: 是否观察(NO=1, YES=2) + temperature: 温度设定(C) """ + # 参数映射 + titration_map = {"NO": "1", "YES": "2", "1": "1", "2": "2"} + torque_map = {"NO": "1", "YES": "2", 1: "1", 2: "2", "1": "1", "2": "2"} + + mapped_titration_type = titration_map.get(str(titration_type), "1") + mapped_torque_variation = int(torque_map.get(str(torque_variation), "1")) + # 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取 if not volume and solvents is not None: # 参数类型转换:如果是字符串则解析为字典 @@ -311,22 +556,22 @@ def liquid_feeding_solvents( if isinstance(temperature, str): temperature = float(temperature) - liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] - observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] + liquid_step_id = self.workflow_step_ids["liquid_feeding_solvents"]["liquid"] + observe_step_id = self.workflow_step_ids["liquid_feeding_solvents"]["observe"] params = { "param_values": { liquid_step_id: { - ACTION_NAMES["liquid_feeding_solvents"]["liquid"]: [ - {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, + self.action_names["liquid_feeding_solvents"]["liquid"]: [ + {"m": 0, "n": 1, "Key": "titrationType", "Value": mapped_titration_type}, {"m": 0, "n": 1, "Key": "volume", "Value": volume}, {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id} ] }, observe_step_id: { - ACTION_NAMES["liquid_feeding_solvents"]["observe"]: [ + self.action_names["liquid_feeding_solvents"]["observe"]: [ {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(mapped_torque_variation)}, {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} ] } @@ -362,10 +607,10 @@ def liquid_feeding_titration( x_value: 手工输入的x值,格式如 "1-2-3" feeding_order_data: feeding_order JSON字符串或对象,用于获取m二酐值 extracted_actuals: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh和actualVolume - titration_type: 是否滴定(1=否, 2=是),默认2 + titration_type: 是否滴定(NO=1, YES=2),默认2 time: 观察时间(分钟) - torque_variation: 是否观察(int类型, 1=否, 2=是) - temperature: 温度(°C) + torque_variation: 是否观察(NO=1, YES=2) + temperature: 温度(C) 自动公式模板: 1000*(m二酐-x)*V二酐滴定/m二酐滴定 其中: @@ -374,6 +619,13 @@ def liquid_feeding_titration( - x = x_value (手工输入) - m二酐 = feeding_order中type为"main_anhydride"的amount值 """ + # 参数映射 + titration_map = {"NO": "1", "YES": "2", "1": "1", "2": "2"} + torque_map = {"NO": "1", "YES": "2", 1: "1", 2: "2", "1": "1", "2": "2"} + + mapped_titration_type = titration_map.get(str(titration_type), "2") + mapped_torque_variation = int(torque_map.get(str(torque_variation), "1")) + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if material_id is None: @@ -460,22 +712,22 @@ def liquid_feeding_titration( elif not volume_formula: raise ValueError("必须提供 volume_formula 或 (x_value + feeding_order_data + extracted_actuals)") - liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] - observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] + liquid_step_id = self.workflow_step_ids["liquid_feeding_titration"]["liquid"] + observe_step_id = self.workflow_step_ids["liquid_feeding_titration"]["observe"] params = { "param_values": { liquid_step_id: { - ACTION_NAMES["liquid_feeding_titration"]["liquid"]: [ + self.action_names["liquid_feeding_titration"]["liquid"]: [ {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, - {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": mapped_titration_type}, {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} ] }, observe_step_id: { - ACTION_NAMES["liquid_feeding_titration"]["observe"]: [ + self.action_names["liquid_feeding_titration"]["observe"]: [ {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(mapped_torque_variation)}, {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} ] } @@ -518,6 +770,89 @@ def _extract_actuals_from_report(self, report) -> Dict[str, Any]: 'actualVolume': actual_volume } + def _simplify_report(self, report) -> Dict[str, Any]: + """简化实验报告,只保留关键信息,去除冗余的工作流参数""" + if not isinstance(report, dict): + return report + + data = report.get('data', {}) + if not isinstance(data, dict): + return report + + # 提取关键信息 + simplified = { + 'name': data.get('name'), + 'code': data.get('code'), + 'requester': data.get('requester'), + 'workflowName': data.get('workflowName'), + 'workflowStep': data.get('workflowStep'), + 'requestTime': data.get('requestTime'), + 'startPreparationTime': data.get('startPreparationTime'), + 'completeTime': data.get('completeTime'), + 'useTime': data.get('useTime'), + 'status': data.get('status'), + 'statusName': data.get('statusName'), + } + + # 提取物料信息(简化版) + pre_intakes = data.get('preIntakes', []) + if pre_intakes and isinstance(pre_intakes, list): + first_intake = pre_intakes[0] + sample_materials = first_intake.get('sampleMaterials', []) + + # 简化物料信息 + simplified_materials = [] + for material in sample_materials: + if isinstance(material, dict): + mat_info = { + 'materialName': material.get('materialName'), + 'materialTypeName': material.get('materialTypeName'), + 'materialCode': material.get('materialCode'), + 'materialLocation': material.get('materialLocation'), + } + + # 解析parameters中的关键信息 + params_str = material.get('parameters', '{}') + try: + params = json.loads(params_str) if isinstance(params_str, str) else params_str + if isinstance(params, dict): + # 只保留关键参数 + if 'density' in params: + mat_info['density'] = params['density'] + if 'feedingHistory' in params: + mat_info['feedingHistory'] = params['feedingHistory'] + if 'liquidVolume' in params: + mat_info['liquidVolume'] = params['liquidVolume'] + if 'm_diamine_tot' in params: + mat_info['m_diamine_tot'] = params['m_diamine_tot'] + if 'wt_diamine' in params: + mat_info['wt_diamine'] = params['wt_diamine'] + except: + pass + + simplified_materials.append(mat_info) + + simplified['sampleMaterials'] = simplified_materials + + # 提取extraProperties中的实际值 + extra_props = first_intake.get('extraProperties', {}) + if isinstance(extra_props, dict): + simplified_extra = {} + for key, value in extra_props.items(): + try: + parsed_value = json.loads(value) if isinstance(value, str) else value + simplified_extra[key] = parsed_value + except: + simplified_extra[key] = value + simplified['extraProperties'] = simplified_extra + + return { + 'data': simplified, + 'code': report.get('code'), + 'message': report.get('message'), + 'timestamp': report.get('timestamp') + } + def extract_actuals_from_batch_reports(self, batch_reports_result: str) -> dict: print(f"[DEBUG] extract_actuals 收到原始数据: {batch_reports_result[:500]}...") # 打印前500字符 try: @@ -671,7 +1006,12 @@ def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = No timeout = int(timeout) if timeout else 7200 check_interval = int(check_interval) if check_interval else 10 if not batch_create_result or batch_create_result == "": - raise ValueError("batch_create_result为空") + raise ValueError( + "batch_create_result参数为空,请确保:\n" + "1. batch_create节点与wait节点之间正确连接了handle\n" + "2. batch_create节点成功执行并返回了结果\n" + "3. 检查上游batch_create任务是否成功创建了订单" + ) try: if isinstance(batch_create_result, str) and '[...]' in batch_create_result: batch_create_result = batch_create_result.replace('[...]', '[]') @@ -687,7 +1027,14 @@ def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = No except Exception as e: raise ValueError(f"解析batch_create_result失败: {e}") if not order_codes or not order_ids: - raise ValueError("缺少order_codes或order_ids") + raise ValueError( + "batch_create_result中未找到order_codes或order_ids,或者为空。\n" + "可能的原因:\n" + "1. batch_create任务执行失败(检查任务是否报错)\n" + "2. 物料配置问题(如'物料样品板分配失败')\n" + "3. Bioyond系统状态异常\n" + f"batch_create_result内容: {batch_create_result[:200]}..." + ) if not isinstance(order_codes, list): order_codes = [order_codes] if not isinstance(order_ids, list): @@ -696,6 +1043,17 @@ def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = No raise ValueError("order_codes与order_ids数量不匹配") total = len(order_codes) pending = {c: {"order_id": order_ids[i], "completed": False} for i, c in enumerate(order_codes)} + + # 发布初始状态事件 + for i, oc in enumerate(order_codes): + self._publish_task_status( + task_id=order_ids[i], + task_code=oc, + task_type="bioyond_workflow", + status="running", + progress=0.0 + ) + reports = [] start_time = time.time() while pending: @@ -711,6 +1069,14 @@ def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = No "extracted": None, "elapsed_time": elapsed_time }) + # 发布超时事件 + self._publish_task_status( + task_id=pending[oc]["order_id"], + task_code=oc, + task_type="bioyond_workflow", + status="timeout", + result={"elapsed_time": elapsed_time} + ) break completed_round = [] for oc in list(pending.keys()): @@ -721,6 +1087,9 @@ def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = No rep = self.hardware_interface.order_report(oid) if not rep: rep = {"error": "无法获取报告"} + else: + # 简化报告,去除冗余信息 + rep = self._simplify_report(rep) reports.append({ "order_code": oc, "order_id": oid, @@ -730,6 +1099,15 @@ def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = No "extracted": self._extract_actuals_from_report(rep), "elapsed_time": elapsed_time }) + # 发布完成事件 + self._publish_task_status( + task_id=oid, + task_code=oc, + task_type="bioyond_workflow", + status="completed", + progress=1.0, + result=rep + ) completed_round.append(oc) del self.order_completion_status[oc] except Exception as e: @@ -743,6 +1121,14 @@ def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = No "error": str(e), "elapsed_time": elapsed_time }) + # 发布错误事件 + self._publish_task_status( + task_id=oid, + task_code=oc, + task_type="bioyond_workflow", + status="error", + result={"error": str(e)} + ) completed_round.append(oc) for oc in completed_round: del pending[oc] @@ -782,9 +1168,16 @@ def liquid_feeding_beaker( assign_material_name: 物料名称(试剂瓶位) time: 观察时间(分钟) torque_variation: 是否观察(int类型, 1=否, 2=是) - titration_type: 是否滴定(1=否, 2=是) - temperature: 温度设定(°C) + titration_type: 是否滴定(NO=1, YES=2) + temperature: 温度设定(C) """ + # 参数映射 + titration_map = {"NO": "1", "YES": "2", "1": "1", "2": "2"} + torque_map = {"NO": "1", "YES": "2", 1: "1", 2: "2", "1": "1", "2": "2"} + + mapped_titration_type = titration_map.get(str(titration_type), "1") + mapped_torque_variation = int(torque_map.get(str(torque_variation), "1")) + self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if material_id is None: @@ -793,22 +1186,22 @@ def liquid_feeding_beaker( if isinstance(temperature, str): temperature = float(temperature) - liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] - observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] + liquid_step_id = self.workflow_step_ids["liquid_feeding_beaker"]["liquid"] + observe_step_id = self.workflow_step_ids["liquid_feeding_beaker"]["observe"] params = { "param_values": { liquid_step_id: { - ACTION_NAMES["liquid_feeding_beaker"]["liquid"]: [ + self.action_names["liquid_feeding_beaker"]["liquid"]: [ {"m": 0, "n": 2, "Key": "volume", "Value": volume}, {"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id}, - {"m": 0, "n": 2, "Key": "titrationType", "Value": titration_type} + {"m": 0, "n": 2, "Key": "titrationType", "Value": mapped_titration_type} ] }, observe_step_id: { - ACTION_NAMES["liquid_feeding_beaker"]["observe"]: [ + self.action_names["liquid_feeding_beaker"]["observe"]: [ {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(mapped_torque_variation)}, {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} ] } @@ -834,11 +1227,18 @@ def drip_back( Args: assign_material_name: 物料名称(液体种类) volume: 分液量(μL) - titration_type: 是否滴定(1=否, 2=是) + titration_type: 是否滴定(NO=1, YES=2) time: 观察时间(分钟) - torque_variation: 是否观察(int类型, 1=否, 2=是) - temperature: 温度(°C) + torque_variation: 是否观察(NO=1, YES=2) + temperature: 温度(C) """ + # 参数映射 + titration_map = {"NO": "1", "YES": "2", "1": "1", "2": "2"} + torque_map = {"NO": "1", "YES": "2", 1: "1", 2: "2", "1": "1", "2": "2"} + + mapped_titration_type = titration_map.get(str(titration_type), "1") + mapped_torque_variation = int(torque_map.get(str(torque_variation), "1")) + self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if material_id is None: @@ -847,22 +1247,22 @@ def drip_back( if isinstance(temperature, str): temperature = float(temperature) - liquid_step_id = WORKFLOW_STEP_IDS["drip_back"]["liquid"] - observe_step_id = WORKFLOW_STEP_IDS["drip_back"]["observe"] + liquid_step_id = self.workflow_step_ids["drip_back"]["liquid"] + observe_step_id = self.workflow_step_ids["drip_back"]["observe"] params = { "param_values": { liquid_step_id: { - ACTION_NAMES["drip_back"]["liquid"]: [ - {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, + self.action_names["drip_back"]["liquid"]: [ + {"m": 0, "n": 1, "Key": "titrationType", "Value": mapped_titration_type}, {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id}, {"m": 0, "n": 1, "Key": "volume", "Value": volume} ] }, observe_step_id: { - ACTION_NAMES["drip_back"]["observe"]: [ + self.action_names["drip_back"]["observe"]: [ {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(mapped_torque_variation)}, {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} ] } @@ -874,6 +1274,53 @@ def drip_back( print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) + def add_time_constraint( + self, + duration: int, + start_step_key: str = "", + end_step_key: str = "", + start_point: int = 0, + end_point: int = 0 + ): + """添加时间约束 + + Args: + duration: 时间(秒) + start_step_key: 起点步骤Key (可选, 默认为空则自动选择) + end_step_key: 终点步骤Key (可选, 默认为空则自动选择) + start_point: 起点计时点 (Start=0, End=1) + end_point: 终点计时点 (Start=0, End=1) + """ + # 参数映射 + point_map = {"Start": 0, "End": 1, 0: 0, 1: 1, "0": 0, "1": 1} + + mapped_start_point = point_map.get(start_point, 0) + mapped_end_point = point_map.get(end_point, 0) + + # 注意:此方法应在添加完起点工作流后,添加终点工作流前调用 + + + current_count = len(self._cached_workflow_sequence) + if current_count == 0: + print("⚠️ 无法添加时间约束:当前没有工作流") + return + + start_index = current_count - 1 + end_index = current_count # 指向下一个即将添加的工作流 + + constraint = { + "start_index": start_index, + "start_step_key": start_step_key, + "end_index": end_index, + "end_step_key": end_step_key, + "duration": duration, + "start_point": mapped_start_point, + "end_point": mapped_end_point + } + self.pending_time_constraints.append(constraint) + print(f"已添加时间约束: Workflow[{start_index}].{start_step_key} -> Workflow[{end_index}].{end_step_key} ({duration}s)") + return json.dumps({"suc": True}) + # ==================== 工作流管理方法 ==================== def get_workflow_sequence(self) -> List[str]: @@ -884,12 +1331,115 @@ def get_workflow_sequence(self) -> List[str]: """ id_to_name = {workflow_id: name for name, workflow_id in self.workflow_mappings.items()} workflow_names = [] - for workflow_id in self.workflow_sequence: + # 使用内部缓存的列表,而不是属性(属性返回 JSON 字符串) + for workflow_id in self._cached_workflow_sequence: workflow_name = id_to_name.get(workflow_id, workflow_id) workflow_names.append(workflow_name) - print(f"工作流序列: {workflow_names}") return workflow_names + def sync_workflow_sequence_from_bioyond(self) -> dict: + """从 Bioyond 系统同步工作流序列 + + 查询 Bioyond 系统中的工作流列表,并更新本地 workflow_sequence + + Returns: + dict: 包含同步结果的字典 + - success: bool, 是否成功 + - workflows: list, 工作流列表 + - message: str, 结果消息 + """ + try: + print(f"[同步工作流序列] 开始从 Bioyond 系统查询工作流...") + + # 检查 hardware_interface 是否可用 + if not hasattr(self, 'hardware_interface') or self.hardware_interface is None: + error_msg = "hardware_interface 未初始化" + print(f"❌ [同步工作流序列] {error_msg}") + return { + "success": False, + "workflows": [], + "message": error_msg + } + + # 查询所有工作流 + query_params = json.dumps({}) + print(f"[同步工作流序列] 调用 hardware_interface.query_workflow...") + workflows_data = self.hardware_interface.query_workflow(query_params) + + print(f"[同步工作流序列] 查询返回数据: {workflows_data}") + + if not workflows_data: + error_msg = "未能从 Bioyond 系统获取工作流数据(返回为空)" + print(f"⚠️ [同步工作流序列] {error_msg}") + return { + "success": False, + "workflows": [], + "message": error_msg + } + + # 获取工作流列表 - Bioyond API 返回的字段是 items,不是 list + workflow_list = workflows_data.get("items", workflows_data.get("list", [])) + print(f"[同步工作流序列] 从 Bioyond 查询到 {len(workflow_list)} 个工作流") + + if len(workflow_list) == 0: + warning_msg = "Bioyond 系统中暂无工作流" + print(f"⚠️ [同步工作流序列] {warning_msg}") + # 清空缓存 + self._cached_workflow_sequence = [] + return { + "success": True, + "workflows": [], + "message": warning_msg + } + + # 清空当前序列 + workflow_ids = [] + + # 构建结果 + synced_workflows = [] + for workflow in workflow_list: + workflow_id = workflow.get("id") + workflow_name = workflow.get("name") + workflow_status = workflow.get("status") # 工作流状态 + + print(f" - 工作流: {workflow_name} (ID: {workflow_id[:8] if workflow_id else 'N/A'}..., 状态: {workflow_status})") + + synced_workflows.append({ + "id": workflow_id, + "name": workflow_name, + "status": workflow_status, + "createTime": workflow.get("createTime"), + "updateTime": workflow.get("updateTime") + }) + + # 添加所有工作流 ID 到执行序列 + if workflow_id: + workflow_ids.append(workflow_id) + + # 更新缓存 + self._cached_workflow_sequence = workflow_ids + + success_msg = f"成功同步 {len(synced_workflows)} 个工作流到本地序列" + print(f"✅ [同步工作流序列] {success_msg}") + print(f"[同步工作流序列] 当前 workflow_sequence: {self._cached_workflow_sequence}") + + return { + "success": True, + "workflows": synced_workflows, + "message": success_msg + } + + except Exception as e: + error_msg = f"从 Bioyond 同步工作流序列失败: {e}" + print(f"❌ [同步工作流序列] {error_msg}") + import traceback + traceback.print_exc() + return { + "success": False, + "workflows": [], + "message": error_msg + } + def workflow_step_query(self, workflow_id: str) -> dict: """查询工作流步骤参数 @@ -912,9 +1462,69 @@ def create_order(self, json_str: str) -> dict: """ return self.hardware_interface.create_order(json_str) + def clear_workflows(self): + """清空缓存的工作流序列和参数""" + self._cached_workflow_sequence = [] + self.pending_time_constraints = [] + print("已清空工作流序列缓存和时间约束队列") + + def clean_all_server_workflows(self) -> Dict[str, Any]: + """ + 清空服务端所有非核心工作流 + 逻辑: + 1. 利用 3.2 接口查询所有工作流 (includeDetail=False) + 2. 提取所有 ID + 3. 利用 3.38 接口 (hard_delete_merged_workflows) 批量删除 + """ + print("正在查询服务端工作流列表...") + try: + # 查询工作流列表 + # 仅需要ID,所以设置 includeDetail=False + query_params = {"includeDetail": False, "type": 0} + query_result = self._post_project_api("/api/lims/workflow/work-flow-list", query_params) + + if query_result.get("code") != 1: + return query_result + + data_obj = query_result.get("data") + + # 处理返回值可能是列表或者分页对象的不同情况 + if isinstance(data_obj, list): + workflows = data_obj + elif isinstance(data_obj, dict): + # 尝试从常见分页字段获取列表 + workflows = data_obj.get("items", data_obj.get("list", [])) + else: + workflows = [] + + if not workflows: + print("无需删除: 服务端无工作流") + return {"code": 1, "message": "服务端无工作流", "timestamp": int(time.time())} + + ids_to_delete = [] + for wf in workflows: + if isinstance(wf, dict): + wf_id = wf.get("id") + if wf_id: + ids_to_delete.append(str(wf_id)) + + if not ids_to_delete: + print("无需删除: 无有效工作流ID") + return {"code": 1, "message": "无有效工作流ID", "timestamp": int(time.time())} + + print(f"查询到 {len(ids_to_delete)} 个工作流,准备调用硬删除接口...") + # 硬删除 + return self.hard_delete_merged_workflows(ids_to_delete) + + except Exception as e: + print(f"❌ 清空工作流业务异常: {str(e)}") + return {"code": 0, "message": str(e), "timestamp": int(time.time())} + def hard_delete_merged_workflows(self, workflow_ids: List[str]) -> Dict[str, Any]: """ - 调用新接口:硬删除合并后的工作流 + 调用新接口:硬删除合并后的工作流 + 根据用户反馈,/api/lims/order/workflows 接口存在校验问题 + 改用 /api/data/order/workflows?workFlowGuids=... 接口 Args: workflow_ids: 要删除的工作流ID数组 @@ -925,7 +1535,30 @@ def hard_delete_merged_workflows(self, workflow_ids: List[str]) -> Dict[str, Any try: if not isinstance(workflow_ids, list): raise ValueError("workflow_ids必须是字符串数组") - return self._delete_project_api("/api/lims/order/workflows", workflow_ids) + + # 使用新 Endpoint: /api/data/order/workflows + endpoint = "/api/data/order/workflows" + url = f"{self.hardware_interface.host}{endpoint}" + + print(f"\n📤 硬删除请求 (Query Param): {url}") + print(f"IDs count: {len(workflow_ids)}") + + # 使用 requests 的 params 传递数组,会生成 workFlowGuids=id1&workFlowGuids=id2 的形式 + params = {"workFlowGuids": workflow_ids} + + response = requests.delete( + url, + params=params, + timeout=60 + ) + + if response.status_code == 200: + print("✅ 删除请求成功") + return {"code": 1, "message": "删除成功", "timestamp": int(time.time())} + else: + print(f"❌ 删除失败: status={response.status_code}, content={response.text}") + return {"code": 0, "message": f"HTTP {response.status_code}: {response.text}", "timestamp": int(time.time())} + except Exception as e: print(f"❌ 硬删除异常: {str(e)}") return {"code": 0, "message": str(e), "timestamp": int(time.time())} @@ -936,14 +1569,14 @@ def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]: """项目接口通用POST调用 参数: - endpoint: 接口路径(例如 /api/lims/order/skip-titration-steps) + endpoint: 接口路径(例如 /api/lims/order/skip-titration-steps) data: 请求体中的 data 字段内容 返回: - dict: 服务端响应,失败时返回 {code:0,message,...} + dict: 服务端响应,失败时返回 {code:0,message,...} """ request_data = { - "apiKey": API_CONFIG["api_key"], + "apiKey": self.bioyond_config["api_key"], "requestTime": self.hardware_interface.get_current_time_iso8601(), "data": data } @@ -976,35 +1609,57 @@ def _delete_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]: """项目接口通用DELETE调用 参数: - endpoint: 接口路径(例如 /api/lims/order/workflows) + endpoint: 接口路径(例如 /api/lims/order/workflows) data: 请求体中的 data 字段内容 返回: - dict: 服务端响应,失败时返回 {code:0,message,...} + dict: 服务端响应,失败时返回 {code:0,message,...} """ request_data = { - "apiKey": API_CONFIG["api_key"], + "apiKey": self.bioyond_config["api_key"], "requestTime": self.hardware_interface.get_current_time_iso8601(), "data": data } print(f"\n📤 项目DELETE请求: {self.hardware_interface.host}{endpoint}") print(json.dumps(request_data, indent=4, ensure_ascii=False)) try: - response = requests.delete( + # 使用 requests.request 显式发送 Body,避免 requests.delete 可能的兼容性问题 + response = requests.request( + "DELETE", f"{self.hardware_interface.host}{endpoint}", - json=request_data, + data=json.dumps(request_data), headers={"Content-Type": "application/json"}, timeout=30 ) - result = response.json() + + try: + result = response.json() + except json.JSONDecodeError: + print(f"❌ 非JSON响应: {response.text}") + return {"code": 0, "message": "非JSON响应", "timestamp": int(time.time())} + if result.get("code") == 1: print("✅ 请求成功") else: - print(f"❌ 请求失败: {result.get('message','未知错误')}") + # 尝试提取详细错误信息 (兼容 Abp 等框架的 error 结构) + msg = result.get('message') + if not msg: + error_obj = result.get('error', {}) + if isinstance(error_obj, dict): + msg = error_obj.get('message') + details = error_obj.get('details') + if details: + msg = f"{msg}: {details}" + + if not msg: + msg = f"未知错误 (Status: {response.status_code})" + + print(f"❌ 请求失败: {msg}") + # 打印完整返回以供调试 + print(f"服务端返回: {json.dumps(result, ensure_ascii=False)}") + return result - except json.JSONDecodeError: - print("❌ 非JSON响应") - return {"code": 0, "message": "非JSON响应", "timestamp": int(time.time())} + except requests.exceptions.Timeout: print("❌ 请求超时") return {"code": 0, "message": "请求超时", "timestamp": int(time.time())} @@ -1030,16 +1685,16 @@ def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]: for name in web_workflow_list: workflow_id = self.workflow_mappings.get(name, "") if not workflow_id: - print(f"警告:未找到工作流名称 {name} 对应的 ID") + print(f"警告:未找到工作流名称 {name} 对应的 ID") continue workflows_result.append({"id": workflow_id, "name": name}) print(f"process_web_workflows 输出: {workflows_result}") return workflows_result except json.JSONDecodeError as e: - print(f"错误:无法解析 web_workflow_json: {e}") + print(f"错误:无法解析 web_workflow_json: {e}") return [] except Exception as e: - print(f"错误:处理工作流失败: {e}") + print(f"错误:处理工作流失败: {e}") return [] def _build_workflows_with_parameters(self, workflows_result: list) -> list: @@ -1047,7 +1702,7 @@ def _build_workflows_with_parameters(self, workflows_result: list) -> list: 构建带参数的工作流列表 Args: - workflows_result: 处理后的工作流列表(应为包含 id 和 name 的字典列表) + workflows_result: 处理后的工作流列表(应为包含 id 和 name 的字典列表) Returns: 符合新接口格式的工作流参数结构 @@ -1059,24 +1714,24 @@ def _build_workflows_with_parameters(self, workflows_result: list) -> list: for idx, workflow_info in enumerate(workflows_result): if not isinstance(workflow_info, dict): - print(f"错误:workflows_result[{idx}] 不是字典,而是 {type(workflow_info)}: {workflow_info}") + print(f"错误:workflows_result[{idx}] 不是字典,而是 {type(workflow_info)}: {workflow_info}") continue workflow_id = workflow_info.get("id") if not workflow_id: - print(f"警告:workflows_result[{idx}] 缺少 'id' 键") + print(f"警告:workflows_result[{idx}] 缺少 'id' 键") continue workflow_name = workflow_info.get("name", "") # print(f"\n🔧 处理工作流 [{idx}]: {workflow_name} (ID: {workflow_id})") if idx >= len(self.pending_task_params): - # print(f" ⚠️ 无对应参数,跳过") + # print(f" ⚠️ 无对应参数,跳过") workflows_with_params.append({"id": workflow_id}) continue param_data = self.pending_task_params[idx] param_values = param_data.get("param_values", {}) if not param_values: - # print(f" ⚠️ 参数为空,跳过") + # print(f" ⚠️ 参数为空,跳过") workflows_with_params.append({"id": workflow_id}) continue @@ -1135,10 +1790,10 @@ def _create_error_result(self, error_msg: str, step: str) -> str: def merge_workflow_with_parameters(self, json_str: str) -> dict: """ - 调用新接口:合并工作流并传递参数 + 调用新接口:合并工作流并传递参数 Args: - json_str: JSON格式的字符串,包含: + json_str: JSON格式的字符串,包含: - name: 工作流名称 - workflows: [{"id": "工作流ID", "stepParameters": {...}}] @@ -1148,7 +1803,7 @@ def merge_workflow_with_parameters(self, json_str: str) -> dict: try: data = json.loads(json_str) - # 在工作流名称后面添加时间戳,避免重复 + # 在工作流名称后面添加时间戳,避免重复 if "name" in data and data["name"]: timestamp = self.hardware_interface.get_current_time_iso8601().replace(":", "-").replace(".", "-") original_name = data["name"] @@ -1156,7 +1811,7 @@ def merge_workflow_with_parameters(self, json_str: str) -> dict: print(f"🕒 工作流名称已添加时间戳: {original_name} -> {data['name']}") request_data = { - "apiKey": API_CONFIG["api_key"], + "apiKey": self.bioyond_config["api_key"], "requestTime": self.hardware_interface.get_current_time_iso8601(), "data": data } @@ -1195,7 +1850,7 @@ def merge_workflow_with_parameters(self, json_str: str) -> dict: return None if result.get("code") == 1: - print(f"✅ 工作流合并成功(带参数)") + print(f"✅ 工作流合并成功(带参数)") return result.get("data", {}) else: error_msg = result.get('message', '未知错误') @@ -1216,7 +1871,7 @@ def merge_workflow_with_parameters(self, json_str: str) -> dict: return None def _validate_and_refresh_workflow_if_needed(self, workflow_name: str) -> bool: - """验证工作流ID是否有效,如果无效则重新合并 + """验证工作流ID是否有效,如果无效则重新合并 Args: workflow_name: 工作流名称 @@ -1225,17 +1880,17 @@ def _validate_and_refresh_workflow_if_needed(self, workflow_name: str) -> bool: bool: 验证或刷新是否成功 """ print(f"\n🔍 验证工作流ID有效性...") - if not self.workflow_sequence: - print(f" ⚠️ 工作流序列为空,需要重新合并") + if not self._cached_workflow_sequence: + print(f" ⚠️ 工作流序列为空,需要重新合并") return False - first_workflow_id = self.workflow_sequence[0] + first_workflow_id = self._cached_workflow_sequence[0] try: structure = self.workflow_step_query(first_workflow_id) if structure: print(f" ✅ 工作流ID有效") return True else: - print(f" ⚠️ 工作流ID已过期,需要重新合并") + print(f" ⚠️ 工作流ID已过期,需要重新合并") return False except Exception as e: print(f" ❌ 工作流ID验证失败: {e}") @@ -1244,7 +1899,7 @@ def _validate_and_refresh_workflow_if_needed(self, workflow_name: str) -> bool: def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict: """ - 一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务 + 一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务 Args: workflow_name: 合并后的工作流名称 @@ -1269,12 +1924,111 @@ def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> di workflows_with_params = self._build_workflows_with_parameters(workflows_result) + # === 构建时间约束 (tcmBs) === + tcm_bs_list = [] + if self.pending_time_constraints: + print(f"\n🔗 处理时间约束 ({len(self.pending_time_constraints)} 个)...") + + + # 建立索引到名称的映射 + workflow_names_by_index = [w["name"] for w in workflows_result] + + # 默认步骤映射表 + DEFAULT_STEP_KEYS = { + "Solid_feeding_vials": "feeding", + "liquid_feeding_beaker": "liquid", + "Liquid_feeding_vials(non-titration)": "liquid", + "Liquid_feeding_solvents": "liquid", + "Liquid_feeding(titration)": "liquid", + "Drip_back": "liquid", + "reactor_taken_in": "config" + } + + for c in self.pending_time_constraints: + try: + start_idx = c["start_index"] + end_idx = c["end_index"] + + if start_idx >= len(workflow_names_by_index) or end_idx >= len(workflow_names_by_index): + print(f" ❌ 约束索引越界: {start_idx} -> {end_idx} (总数: {len(workflow_names_by_index)})") + continue + + start_wf_name = workflow_names_by_index[start_idx] + end_wf_name = workflow_names_by_index[end_idx] + + # 辅助函数:根据名称查找 config 中的 key + def find_config_key(name): + # 1. 直接匹配 + if name in self.workflow_step_ids: + return name + # 2. 尝试反向查找 WORKFLOW_TO_SECTION_MAP (如果需要) + # 3. 尝试查找 WORKFLOW_MAPPINGS 的 key (忽略大小写匹配或特定映射) + + # 硬编码常见映射 (Web名称 -> Config Key) + mapping = { + "Solid_feeding_vials": "solid_feeding_vials", + "Liquid_feeding_vials(non-titration)": "liquid_feeding_vials_non_titration", + "Liquid_feeding_solvents": "liquid_feeding_solvents", + "Liquid_feeding(titration)": "liquid_feeding_titration", + "Drip_back": "drip_back" + } + return mapping.get(name, name) + + start_config_key = find_config_key(start_wf_name) + end_config_key = find_config_key(end_wf_name) + + # 查找 UUID + if start_config_key not in self.workflow_step_ids: + print(f" ❌ 找不到工作流 {start_wf_name} (Key: {start_config_key}) 的步骤配置") + continue + if end_config_key not in self.workflow_step_ids: + print(f" ❌ 找不到工作流 {end_wf_name} (Key: {end_config_key}) 的步骤配置") + continue + + # 确定步骤 Key + start_key = c["start_step_key"] + if not start_key: + start_key = DEFAULT_STEP_KEYS.get(start_wf_name) + if not start_key: + print(f" ❌ 未指定起点步骤Key且无默认值: {start_wf_name}") + continue + + end_key = c["end_step_key"] + if not end_key: + end_key = DEFAULT_STEP_KEYS.get(end_wf_name) + if not end_key: + print(f" ❌ 未指定终点步骤Key且无默认值: {end_wf_name}") + continue + + start_step_id = self.workflow_step_ids[start_config_key].get(start_key) + end_step_id = self.workflow_step_ids[end_config_key].get(end_key) + + if not start_step_id or not end_step_id: + print(f" ❌ 无法解析步骤ID: {start_config_key}.{start_key} -> {end_config_key}.{end_key}") + continue + + tcm_bs_list.append({ + "startWorkflowIndex": start_idx, + "startStepId": start_step_id, + "startComparePoint": c["start_point"], + "endWorkflowIndex": end_idx, + "endStepId": end_step_id, + "endComparePoint": c["end_point"], + "ct": c["duration"], + "description": f"Constraint {start_idx}->{end_idx}" + }) + print(f" ✅ 添加约束: {start_wf_name}({start_key}) -> {end_wf_name}({end_key})") + + except Exception as e: + print(f" ❌ 处理约束时出错: {e}") + merge_data = { "name": workflow_name, - "workflows": workflows_with_params + "workflows": workflows_with_params, + "tcmBs": tcm_bs_list } - # print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}") + # print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}") merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data)) if not merged_workflow: @@ -1291,20 +2045,28 @@ def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> di "paramValues": {} }] - result = self.create_order(json.dumps(order_params)) - - if not result: - return self._create_error_result("创建任务失败", "create_order") - - # 清空工作流序列和参数,防止下次执行时累积重复 - self.pending_task_params = [] - self.clear_workflows() # 清空工作流序列,避免重复累积 + # 尝试创建订单:无论成功或失败,都需要在本次尝试结束后清理本地队列,避免下一次重复累积 + try: + result = self.create_order(json.dumps(order_params)) + if not result: + # 返回错误结果之前先记录情况(稍后由 finally 清理队列) + print("⚠️ 创建任务返回空或失败响应,稍后将清理本地队列以避免重复累积") + return self._create_error_result("创建任务失败", "create_order") + finally: + # 无论任务创建成功与否,都要清空本地保存的参数和工作流序列,防止下次重复 + try: + self.pending_task_params = [] + self.clear_workflows() # 清空工作流序列,避免重复累积 + print("✅ 已清理 pending_task_params 与 workflow_sequence") + except Exception as _ex: + # 记录清理失败,但不要阻塞原始返回 + print(f"❌ 清理队列时发生异常: {_ex}") # print(f"\n✅ 任务创建成功: {result}") # print(f"\n✅ 任务创建成功") print(f"{'='*60}\n") - # 返回结果,包含合并后的工作流数据和订单参数 + # 返回结果,包含合并后的工作流数据和订单参数 return json.dumps({ "success": True, "result": result, @@ -1321,10 +2083,42 @@ def skip_titration_steps(self, preintake_id: str) -> Dict[str, Any]: preintake_id: 通量ID Returns: - Dict[str, Any]: 服务器响应,包含状态码、消息和时间戳 + Dict[str, Any]: 服务器响应,包含状态码,消息和时间戳 """ try: return self._post_project_api("/api/lims/order/skip-titration-steps", preintake_id) except Exception as e: print(f"❌ 跳过滴定异常: {str(e)}") return {"code": 0, "message": str(e), "timestamp": int(time.time())} + + def set_reactor_temperature(self, reactor_id: int, temperature: float) -> str: + """ + 设置反应器温度 + + Args: + reactor_id: 反应器编号 (1-5) + temperature: 目标温度 (°C) + + Returns: + str: JSON 字符串,格式为 {"suc": True/False, "msg": "描述信息"} + """ + if reactor_id not in range(1, 6): + return json.dumps({"suc": False, "msg": "反应器编号必须在 1-5 之间"}) + + try: + payload = { + "deviceTypeName": f"反应模块{chr(64 + reactor_id)}", # 1->A, 2->B... + "temperature": float(temperature) + } + resp = requests.post( + f"{self.hardware_interface.host}/api/lims/device/set-reactor-temperatue", + json=payload, + headers={"Content-Type": "application/json"}, + timeout=10 + ) + if resp.status_code == 200: + return json.dumps({"suc": True, "msg": "温度设置成功"}) + else: + return json.dumps({"suc": False, "msg": f"温度设置失败,HTTP {resp.status_code}"}) + except Exception as e: + return json.dumps({"suc": False, "msg": f"温度设置异常: {str(e)}"}) diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index e349b083..327d8195 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -6,6 +6,7 @@ """ import time import traceback +import threading from datetime import datetime from typing import Dict, Any, List, Optional, Union import json @@ -23,12 +24,94 @@ from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String from pylabrobot.resources.resource import Resource as ResourcePLR -from unilabos.devices.workstation.bioyond_studio.config import ( - API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, HTTP_SERVICE_CONFIG -) + from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService +class ConnectionMonitor: + """Bioyond连接监控器""" + def __init__(self, workstation, check_interval=30): + self.workstation = workstation + self.check_interval = check_interval + self._running = False + self._thread = None + self._last_status = "unknown" + + def start(self): + if self._running: + return + self._running = True + self._thread = threading.Thread(target=self._monitor_loop, daemon=True, name="BioyondConnectionMonitor") + self._thread.start() + logger.info("Bioyond连接监控器已启动") + + def stop(self): + self._running = False + if self._thread: + self._thread.join(timeout=2) + logger.info("Bioyond连接监控器已停止") + + def _monitor_loop(self): + while self._running: + try: + # 使用 lightweight API 检查连接 + # query_matial_type_list 是比较快的查询 + start_time = time.time() + result = self.workstation.hardware_interface.material_type_list() + + status = "online" if result else "offline" + msg = "Connection established" if status == "online" else "Failed to get material type list" + + if status != self._last_status: + logger.info(f"Bioyond连接状态变更: {self._last_status} -> {status}") + self._publish_event(status, msg) + self._last_status = status + + # 发布心跳 (可选,或者只在状态变更时发布) + # self._publish_event(status, msg) + + except Exception as e: + logger.error(f"Bioyond连接检查异常: {e}") + if self._last_status != "error": + self._publish_event("error", str(e)) + self._last_status = "error" + + time.sleep(self.check_interval) + + def _publish_event(self, status, message): + try: + if hasattr(self.workstation, "_ros_node") and self.workstation._ros_node: + event_data = { + "status": status, + "message": message, + "timestamp": datetime.now().isoformat() + } + + # 动态发布消息,需要在 ROS2DeviceNode 中有对应支持 + # 这里假设通用事件发布机制,使用 String 类型的 topic + # 话题: //events/device_status + ns = self.workstation._ros_node.namespace + topic = f"{ns}/events/device_status" + + # 使用 ROS2DeviceNode 的发布功能 + # 如果没有预定义的 publisher,需要动态创建 + # 注意:workstation base node 可能没有自动创建 arbitrary publishers 的机制 + # 这里我们先尝试用 String json 发布 + + # 在 ROS2DeviceNode 中通常需要先 create_publisher + # 为了简单起见,我们检查是否已有 publisher,没有则创建 + if not hasattr(self.workstation, "_device_status_pub"): + self.workstation._device_status_pub = self.workstation._ros_node.create_publisher( + String, topic, 10 + ) + + self.workstation._device_status_pub.publish( + convert_to_ros_msg(String, json.dumps(event_data, ensure_ascii=False)) + ) + except Exception as e: + logger.error(f"发布设备状态事件失败: {e}") + + class BioyondResourceSynchronizer(ResourceSynchronizer): """Bioyond资源同步器 @@ -174,9 +257,8 @@ def sync_to_external(self, resource: Any) -> bool: else: logger.info(f"[同步→Bioyond] ➕ 物料不存在于 Bioyond,将创建新物料并入库") - # 第1步:获取仓库配置 - from .config import WAREHOUSE_MAPPING - warehouse_mapping = WAREHOUSE_MAPPING + # 第1步:从配置中获取仓库配置 + warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) # 确定目标仓库名称 parent_name = None @@ -238,14 +320,20 @@ def sync_to_external(self, resource: Any) -> bool: # 第2步:转换为 Bioyond 格式 logger.info(f"[同步→Bioyond] 🔄 转换物料为 Bioyond 格式...") - # 导入物料默认参数配置 - from .config import MATERIAL_DEFAULT_PARAMETERS + # 从配置中获取物料默认参数 + material_default_params = self.workstation.bioyond_config.get("material_default_parameters", {}) + material_type_params = self.workstation.bioyond_config.get("material_type_parameters", {}) + + # 合并参数配置:物料名称参数 + typeId参数(转换为 type: 格式) + merged_params = material_default_params.copy() + for type_id, params in material_type_params.items(): + merged_params[f"type:{type_id}"] = params bioyond_material = resource_plr_to_bioyond( [resource], type_mapping=self.workstation.bioyond_config["material_type_mappings"], warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"], - material_params=MATERIAL_DEFAULT_PARAMETERS + material_params=merged_params )[0] logger.info(f"[同步→Bioyond] 🔧 准备覆盖locations字段,目标仓库: {parent_name}, 库位: {update_site}, UUID: {target_location_uuid[:8]}...") @@ -468,13 +556,20 @@ def _create_material_only(self, resource: Any) -> Optional[str]: return material_bioyond_id # 转换为 Bioyond 格式 - from .config import MATERIAL_DEFAULT_PARAMETERS + # 从配置中获取物料默认参数 + material_default_params = self.workstation.bioyond_config.get("material_default_parameters", {}) + material_type_params = self.workstation.bioyond_config.get("material_type_parameters", {}) + + # 合并参数配置:物料名称参数 + typeId参数(转换为 type: 格式) + merged_params = material_default_params.copy() + for type_id, params in material_type_params.items(): + merged_params[f"type:{type_id}"] = params bioyond_material = resource_plr_to_bioyond( [resource], type_mapping=self.workstation.bioyond_config["material_type_mappings"], warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"], - material_params=MATERIAL_DEFAULT_PARAMETERS + material_params=merged_params )[0] # ⚠️ 关键:创建物料时不设置 locations,让 Bioyond 系统暂不分配库位 @@ -528,8 +623,7 @@ def _inbound_material_only(self, resource: Any, material_id: str) -> bool: logger.info(f"[物料入库] 目标库位: {update_site}") # 获取仓库配置和目标库位 UUID - from .config import WAREHOUSE_MAPPING - warehouse_mapping = WAREHOUSE_MAPPING + warehouse_mapping = self.workstation.bioyond_config.get("warehouse_mapping", {}) parent_name = None target_location_uuid = None @@ -584,6 +678,44 @@ class BioyondWorkstation(WorkstationBase): 集成Bioyond物料管理的工作站实现 """ + def _publish_task_status( + self, + task_id: str, + task_type: str, + status: str, + result: dict = None, + progress: float = 0.0, + task_code: str = None + ): + """发布任务状态事件""" + try: + if not getattr(self, "_ros_node", None): + return + + event_data = { + "task_id": task_id, + "task_code": task_code, + "task_type": task_type, + "status": status, + "progress": progress, + "timestamp": datetime.now().isoformat() + } + if result: + event_data["result"] = result + + topic = f"{self._ros_node.namespace}/events/task_status" + + if not hasattr(self, "_task_status_pub"): + self._task_status_pub = self._ros_node.create_publisher( + String, topic, 10 + ) + + self._task_status_pub.publish( + convert_to_ros_msg(String, json.dumps(event_data, ensure_ascii=False)) + ) + except Exception as e: + logger.error(f"发布任务状态事件失败: {e}") + def __init__( self, bioyond_config: Optional[Dict[str, Any]] = None, @@ -605,10 +737,28 @@ def __init__( raise ValueError("Deck 配置不能为空,请在配置文件中添加正确的 deck 配置") # 初始化 warehouses 属性 - self.deck.warehouses = {} - for resource in self.deck.children: - if isinstance(resource, WareHouse): - self.deck.warehouses[resource.name] = resource + if not hasattr(self.deck, "warehouses") or self.deck.warehouses is None: + self.deck.warehouses = {} + + # 仅当 warehouses 为空时尝试重新扫描(避免覆盖子类的修复) + if not self.deck.warehouses: + for resource in self.deck.children: + # 兼容性增强: 只要是仓库类别或者是 WareHouse 实例均可 + is_warehouse = isinstance(resource, WareHouse) or getattr(resource, "category", "") == "warehouse" + + # 如果配置中有定义,也可以认定为 warehouse + if not is_warehouse and "warehouse_mapping" in bioyond_config: + if resource.name in bioyond_config["warehouse_mapping"]: + is_warehouse = True + + if is_warehouse: + self.deck.warehouses[resource.name] = resource + # 确保 category 被正确设置,方便后续使用 + if getattr(resource, "category", "") != "warehouse": + try: + resource.category = "warehouse" + except: + pass # 创建通信模块 self._create_communication_module(bioyond_config) @@ -627,18 +777,22 @@ def __init__( self._set_workflow_mappings(bioyond_config["workflow_mappings"]) # 准备 HTTP 报送接收服务配置(延迟到 post_init 启动) - # 从 bioyond_config 中获取,如果没有则使用 HTTP_SERVICE_CONFIG 的默认值 + # 从 bioyond_config 中的 http_service_config 获取 + http_service_cfg = bioyond_config.get("http_service_config", {}) self._http_service_config = { - "host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG["http_service_host"]), - "port": bioyond_config.get("http_service_port", HTTP_SERVICE_CONFIG["http_service_port"]) + "host": http_service_cfg.get("http_service_host", "127.0.0.1"), + "port": http_service_cfg.get("http_service_port", 8080) } - self.http_service = None # 将在 post_init 中启动 + self.http_service = None # 将在 post_init 启动 + self.connection_monitor = None # 将在 post_init 启动 logger.info(f"Bioyond工作站初始化完成") def __del__(self): """析构函数:清理资源,停止 HTTP 服务""" try: + if hasattr(self, 'connection_monitor') and self.connection_monitor: + self.connection_monitor.stop() if hasattr(self, 'http_service') and self.http_service is not None: logger.info("正在停止 HTTP 报送服务...") self.http_service.stop() @@ -648,8 +802,19 @@ def __del__(self): def post_init(self, ros_node: ROS2WorkstationNode): self._ros_node = ros_node + # 启动连接监控 + try: + self.connection_monitor = ConnectionMonitor(self) + self.connection_monitor.start() + except Exception as e: + logger.error(f"启动连接监控失败: {e}") + # 启动 HTTP 报送接收服务(现在 device_id 已可用) - if hasattr(self, '_http_service_config'): + # ⚠️ 检查子类是否已经自己管理 HTTP 服务 + if self.bioyond_config.get("_disable_auto_http_service"): + logger.info("🔧 检测到 _disable_auto_http_service 标志,跳过自动启动 HTTP 服务") + logger.info(" 子类(BioyondCellWorkstation)已自行管理 HTTP 服务") + elif hasattr(self, '_http_service_config'): try: self.http_service = WorkstationHTTPService( workstation_instance=self, @@ -688,19 +853,14 @@ def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resou def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: """创建Bioyond通信模块""" - # 创建默认配置 - default_config = { - **API_CONFIG, - "workflow_mappings": WORKFLOW_MAPPINGS, - "material_type_mappings": MATERIAL_TYPE_MAPPINGS, - "warehouse_mapping": WAREHOUSE_MAPPING - } - - # 如果传入了 config,合并配置(config 中的值会覆盖默认值) + # 直接使用传入的配置,不再使用默认值 + # 所有配置必须从 JSON 文件中提供 if config: - self.bioyond_config = {**default_config, **config} + self.bioyond_config = config else: - self.bioyond_config = default_config + # 如果没有配置,使用空字典(会导致后续错误,但这是预期的) + self.bioyond_config = {} + print("警告: 未提供 bioyond_config,请确保在 JSON 配置文件中提供完整配置") self.hardware_interface = BioyondV1RPC(self.bioyond_config) @@ -1014,7 +1174,15 @@ def append_to_workflow_sequence(self, web_workflow_name: str) -> bool: workflow_id = self._get_workflow(actual_workflow_name) if workflow_id: - self.workflow_sequence.append(workflow_id) + # 兼容 BioyondReactionStation 中 workflow_sequence 被重写为 property 的情况 + if isinstance(self.workflow_sequence, list): + self.workflow_sequence.append(workflow_id) + elif hasattr(self, "_cached_workflow_sequence") and isinstance(self._cached_workflow_sequence, list): + self._cached_workflow_sequence.append(workflow_id) + else: + print(f"❌ 无法添加工作流: workflow_sequence 类型错误 {type(self.workflow_sequence)}") + return False + print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}") return True return False @@ -1215,6 +1383,22 @@ def process_step_finish_report(self, report_request) -> Dict[str, Any]: # TODO: 根据实际业务需求处理步骤完成逻辑 # 例如:更新数据库、触发后续流程等 + # 发布任务状态事件 (running/progress update) + self._publish_task_status( + task_id=data.get('orderCode'), # 使用 OrderCode 作为关联 ID + task_code=data.get('orderCode'), + task_type="bioyond_step", + status="running", + progress=0.5, # 步骤完成视为任务进行中 + result={"step_name": data.get('stepName'), "step_id": data.get('stepId')} + ) + + # 更新物料信息 + # 步骤完成后,物料状态可能发生变化(如位置、用量等),触发同步 + logger.info(f"[步骤完成报送] 触发物料同步...") + self.resource_synchronizer.sync_from_external() + + return { "processed": True, "step_id": data.get('stepId'), @@ -1249,6 +1433,17 @@ def process_sample_finish_report(self, report_request) -> Dict[str, Any]: # TODO: 根据实际业务需求处理通量完成逻辑 + # 发布任务状态事件 + self._publish_task_status( + task_id=data.get('orderCode'), + task_code=data.get('orderCode'), + task_type="bioyond_sample", + status="running", + progress=0.7, + result={"sample_id": data.get('sampleId'), "status": status_desc} + ) + + return { "processed": True, "sample_id": data.get('sampleId'), @@ -1288,6 +1483,32 @@ def process_order_finish_report(self, report_request, used_materials: List) -> D # TODO: 根据实际业务需求处理任务完成逻辑 # 例如:更新物料库存、生成报表等 + # 映射状态到事件状态 + event_status = "completed" + if str(data.get('status')) in ["-11", "-12"]: + event_status = "error" + elif str(data.get('status')) == "30": + event_status = "completed" + else: + event_status = "running" # 其他状态视为运行中(或根据实际定义) + + # 发布任务状态事件 + self._publish_task_status( + task_id=data.get('orderCode'), + task_code=data.get('orderCode'), + task_type="bioyond_order", + status=event_status, + progress=1.0 if event_status in ["completed", "error"] else 0.9, + result={"order_name": data.get('orderName'), "status": status_desc, "materials_count": len(used_materials)} + ) + + # 更新物料信息 + # 任务完成后,且状态为完成时,触发同步以更新最终物料状态 + if event_status == "completed": + logger.info(f"[任务完成报送] 触发物料同步...") + self.resource_synchronizer.sync_from_external() + + return { "processed": True, "order_code": data.get('orderCode'), diff --git a/unilabos/devices/workstation/coin_cell_assembly/20251230_Modbus_CSV_Mapping_Guide.md b/unilabos/devices/workstation/coin_cell_assembly/20251230_Modbus_CSV_Mapping_Guide.md new file mode 100644 index 00000000..8af63e7a --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/20251230_Modbus_CSV_Mapping_Guide.md @@ -0,0 +1,84 @@ +# Modbus CSV 地址映射说明 + +本文档说明 `coin_cell_assembly_a.csv` 文件如何将命名节点映射到实际的 Modbus 地址,以及如何在代码中使用它们。 + +## 1. CSV 文件结构 + +地址表文件位于同级目录下:`coin_cell_assembly_a.csv` + +每一行定义了一个 Modbus 节点,包含以下关键列: + +| 列名 | 说明 | 示例 | +|------|------|------| +| **Name** | **节点名称** (代码中引用的 Key) | `COIL_ALUMINUM_FOIL` | +| **DataType** | 数据类型 (BOOL, INT16, FLOAT32, STRING) | `BOOL` | +| **Comment** | 注释说明 | `使用铝箔垫` | +| **Attribute** | 属性 (通常留空或用于额外标记) | | +| **DeviceType** | Modbus 寄存器类型 (`coil`, `hold_register`) | `coil` | +| **Address** | **Modbus 地址** (十进制) | `8340` | + +### 示例行 (铝箔垫片) + +```csv +COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340, +``` + +- **名称**: `COIL_ALUMINUM_FOIL` +- **类型**: `coil` (线圈,读写单个位) +- **地址**: `8340` + +--- + +## 2. 加载与注册流程 + +在 `coin_cell_assembly.py` 的初始化代码中: + +1. **加载 CSV**: `BaseClient.load_csv()` 读取 CSV 并解析每行定义。 +2. **注册节点**: `modbus_client.register_node_list()` 将解析后的节点注册到 Modbus 客户端实例中。 + +```python +# 代码位置: coin_cell_assembly.py (L174-175) +self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv')) +self.client = modbus_client.register_node_list(self.nodes) +``` + +--- + +## 3. 代码中的使用方式 + +注册后,通过 `self.client.use_node('节点名称')` 即可获取该节点对象并进行读写操作,无需关心具体地址。 + +### 控制铝箔垫片 (COIL_ALUMINUM_FOIL) + +```python +# 代码位置: qiming_coin_cell_code 函数 (L1048) +self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian) +``` + +- **写入 True**: 对应 Modbus 功能码 05 (Write Single Coil),向地址 `8340` 写入 `1` (ON)。 +- **写入 False**: 向地址 `8340` 写入 `0` (OFF)。 + +> **注意**: 代码中使用了 `not lvbodian`,这意味着逻辑是反转的。如果 `lvbodian` 参数为 `True` (默认),写入的是 `False` (不使用铝箔垫)。 + +--- + +## 4. 地址转换注意事项 (Modbus vs PLC) + +CSV 中的 `Address` 列(如 `8340`)是 **Modbus 协议地址**。 + +如果使用 InoProShop (汇川 PLC 编程软件),看到的可能是 **PLC 内部地址** (如 `%QX...` 或 `%MW...`)。这两者之间通常需要转换。 + +### 常见的转换规则 (示例) + +- **Coil (线圈) %QX**: + - `Modbus地址 = 字节地址 * 8 + 位偏移` + - *例子*: `%QX834.0` -> `834 * 8 + 0` = `6672` + - *注意*: 如果 CSV 中配置的是 `8340`,这可能是一个自定义映射,或者是基于不同规则(如直接对应 Word 地址的某种映射,或者可能就是地址写错了/使用了非标准映射)。 + +- **Register (寄存器) %MW**: + - 通常直接对应,或者有偏移量 (如 Modbus 40001 = PLC MW0)。 + +### 验证方法 +由于 `test_unilab_interact.py` 中发现 `8450` (CSV风格) 不工作,而 `6760` (%QX845.0 计算值) 工作正常,**建议对 CSV 中的其他地址也进行核实**,特别是像 `8340` 这样以 0 结尾看起来像是 "字节地址+0" 的数值,可能实际上应该是 `%QX834.0` 对应的 `6672`。 + +如果发现设备控制无反应,请尝试按照标准的 Modbus 计算方式转换 PLC 地址。 diff --git "a/unilabos/devices/workstation/coin_cell_assembly/20260113_\347\211\251\346\226\231\346\220\234\345\257\273\347\241\256\350\256\244\345\274\271\347\252\227\350\207\252\345\212\250\345\244\204\347\220\206\345\212\237\350\203\275.md" "b/unilabos/devices/workstation/coin_cell_assembly/20260113_\347\211\251\346\226\231\346\220\234\345\257\273\347\241\256\350\256\244\345\274\271\347\252\227\350\207\252\345\212\250\345\244\204\347\220\206\345\212\237\350\203\275.md" new file mode 100644 index 00000000..96104b6f --- /dev/null +++ "b/unilabos/devices/workstation/coin_cell_assembly/20260113_\347\211\251\346\226\231\346\220\234\345\257\273\347\241\256\350\256\244\345\274\271\347\252\227\350\207\252\345\212\250\345\244\204\347\220\206\345\212\237\350\203\275.md" @@ -0,0 +1,352 @@ +# 2026-01-13 物料搜寻确认弹窗自动处理功能 + +## 概述 + +本次更新为设备初始化流程添加了**物料搜寻确认弹窗自动检测与处理功能**。在设备初始化过程中,PLC 会弹出物料搜寻确认对话框,现在系统可以根据用户参数自动点击"是"或"否"按钮,无需手动干预。 + +## 背景问题 + +### 原有流程 +1. 调用 `func_pack_device_init_auto_start_combined()` 初始化设备 +2. PLC 在初始化过程中弹出物料搜寻确认对话框 +3. **需要人工手动点击**"是"或"否"按钮 +4. PLC 继续完成初始化并启动 + +### 存在的问题 +- 需要人工干预,无法实现全自动化 +- 影响批量生产效率 +- 容易遗忘点击导致流程卡住 + +## 解决方案 + +### 新增 Modbus 地址配置 + +在 `coin_cell_assembly_b.csv` 第 69-71 行添加三个 coil: + +| Name | DeviceType | Address | 说明 | +|------|-----------|---------|------| +| COIL_MATERIAL_SEARCH_DIALOG_APPEAR | coil | 6470 | 物料搜寻确认弹窗画面是否出现 | +| COIL_MATERIAL_SEARCH_CONFIRM_YES | coil | 6480 | 初始化物料搜寻确认按钮"是" | +| COIL_MATERIAL_SEARCH_CONFIRM_NO | coil | 6490 | 初始化物料搜寻确认按钮"否" | + +**Modbus 地址转换:** +- CSV 6470 → Modbus 5176 (弹窗出现) +- CSV 6480 → Modbus 5184 (按钮"是") +- CSV 6490 → Modbus 5192 (按钮"否") + +## 代码修改详情 + +### 1. coin_cell_assembly.py + +#### 1.1 新增辅助方法 `_handle_material_search_dialog()` + +**位置:** 第 799-901 行 + +**功能:** +- 监测物料搜寻确认弹窗是否出现(Coil 5176) +- 根据 `enable_search` 参数自动点击对应按钮 +- 使用**脉冲模式**模拟真实按钮操作:`True` → 保持 0.5 秒 → `False` + +**参数:** +- `enable_search: bool` - True=点击"是"(启用物料搜寻), False=点击"否"(不启用) +- `timeout: int = 30` - 等待弹窗出现的最大时间(秒) + +**逻辑流程:** +```python +1. 监测 COIL_MATERIAL_SEARCH_DIALOG_APPEAR (每 0.5 秒检查一次) +2. 检测到弹窗出现 (Coil = True) +3. 选择按钮: + - enable_search=True → COIL_MATERIAL_SEARCH_CONFIRM_YES + - enable_search=False → COIL_MATERIAL_SEARCH_CONFIRM_NO +4. 执行脉冲操作: + - 写入 True (按下按钮) + - 等待 0.5 秒 + - 写入 False (释放按钮) + - 验证状态 +``` + +#### 1.2 修改 `func_pack_device_init_auto_start_combined()` + +**位置:** 第 904-1115 行 + +**主要改动:** + +1. **添加新参数** + ```python + def func_pack_device_init_auto_start_combined( + self, + material_search_enable: bool = False # 新增参数 + ) -> bool: + ``` + +2. **内联初始化逻辑并集成弹窗检测** + - 不再调用 `self.func_pack_device_init()` + - 将初始化逻辑直接实现在函数内 + - **在等待初始化完成的循环中实时检测弹窗** + - 避免死锁:PLC 等待弹窗确认 ↔ 代码等待初始化完成 + +3. **关键代码片段** + ```python + # 等待初始化完成,同时检测物料搜寻弹窗 + while (self._sys_init_status()) == False: + # 检查超时 + if time.time() - start_wait > max_wait_time: + raise RuntimeError(f"初始化超时") + + # 如果还没处理弹窗,检测弹窗是否出现 + if not dialog_handled: + dialog_state = self.client.use_node('COIL_MATERIAL_SEARCH_DIALOG_APPEAR').read(1) + if dialog_actual: # 弹窗出现 + # 执行脉冲按钮点击 + button_node.write(True) # 按下 + time.sleep(0.5) # 保持 + button_node.write(False) # 释放 + dialog_handled = True + + time.sleep(1) + ``` + +4. **步骤调整** + - 步骤 0: 前置条件检查 + - 步骤 1: 设备初始化(**包含弹窗检测**) + - 步骤 1.5: 已在步骤 1 中完成 + - 步骤 2: 切换自动模式 + - 步骤 3: 启动设备 + +### 2. coin_cell_workstation.yaml + +**位置:** 第 292-312 行 + +**修改内容:** + +```yaml +auto-func_pack_device_init_auto_start_combined: + goal_default: + material_search_enable: false # 新增默认值 + + schema: + description: 组合函数:设备初始化 + 物料搜寻确认 + 切换自动模式 + 启动。初始化过程中会自动检测物料搜寻确认弹窗,并根据参数自动点击"是"或"否"按钮 + + goal: + properties: + material_search_enable: # 新增参数配置 + default: false + description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻) + type: boolean +``` + +### 3. 测试脚本(已创建,用户已删除) + +#### 3.1 test_material_search_dialog.py +- 从 CSV 动态加载 Modbus 地址 +- 支持 4 种测试模式: + - `query` - 查询所有状态 + - `dialog <0|1>` - 设置弹窗出现/消失 + - `yes` - 脉冲点击"是"按钮 + - `no` - 脉冲点击"否"按钮 +- 兼容 pymodbus 3.x API + +#### 3.2 更新其他测试脚本 +- `test_coin_cell_reset.py` - 更新为 pymodbus 3.x API +- `test_unilab_interact.py` - 更新为 pymodbus 3.x API + +## 使用方法 + +### 参数说明 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `material_search_enable` | boolean | `false` | 是否启用物料搜寻功能 | + +### 调用示例 + +#### 1. 不启用物料搜寻(默认) +```python +# 默认参数,点击"否"按钮 +await device.func_pack_device_init_auto_start_combined() +``` + +或在 YAML workflow 中: +```yaml +# 使用默认值 false,不启用物料搜寻 +- BatteryStation/auto-func_pack_device_init_auto_start_combined: {} +``` + +#### 2. 启用物料搜寻 +```python +# 显式设置为 True,点击"是"按钮 +await device.func_pack_device_init_auto_start_combined( + material_search_enable=True +) +``` + +或在 YAML workflow 中: +```yaml +- BatteryStation/auto-func_pack_device_init_auto_start_combined: + goal: + material_search_enable: true # 启用物料搜寻 +``` + +## 执行日志示例 + +``` +26-01-13 [21:32:44] [INFO] 开始组合操作:设备初始化 → 物料搜寻确认 → 自动模式 → 启动 +26-01-13 [21:32:44] [INFO] 【步骤 0/4】前置条件检查... +26-01-13 [21:32:44] [INFO] ✓ REG_UNILAB_INTERACT 检查通过 +26-01-13 [21:32:44] [INFO] ✓ COIL_GB_L_IGNORE_CMD 检查通过 +26-01-13 [21:32:44] [INFO] 【步骤 1/4】设备初始化... +26-01-13 [21:32:44] [INFO] 切换手动模式... +26-01-13 [21:32:46] [INFO] 发送初始化命令... +26-01-13 [21:32:47] [INFO] 等待初始化完成(同时监测物料搜寻弹窗)... +26-01-13 [21:33:05] [INFO] ✓ 在初始化过程中检测到物料搜寻确认弹窗! +26-01-13 [21:33:05] [INFO] 用户选择: 不启用物料搜寻(点击否) +26-01-13 [21:33:05] [INFO] → 按下按钮 '否' +26-01-13 [21:33:06] [INFO] → 释放按钮 '否' +26-01-13 [21:33:07] [INFO] ✓ 成功处理物料搜寻确认弹窗(选择: 否) +26-01-13 [21:33:08] [INFO] ✓ 初始化状态完成 +26-01-13 [21:33:12] [INFO] ✓ 设备初始化完成 +26-01-13 [21:33:12] [INFO] 【步骤 1.5/4】物料搜寻确认已在初始化过程中完成 +26-01-13 [21:33:12] [INFO] 【步骤 2/4】切换自动模式... +26-01-13 [21:33:15] [INFO] ✓ 切换自动模式完成 +26-01-13 [21:33:15] [INFO] 【步骤 3/4】启动设备... +26-01-13 [21:33:18] [INFO] ✓ 启动设备完成 +26-01-13 [21:33:18] [INFO] 组合操作完成:设备已成功初始化、确认物料搜寻、切换自动模式并启动 +``` + +## 技术要点 + +### 1. 脉冲模式按钮操作 +模拟真实按钮按压过程: +1. 写入 `True` (按下) +2. 保持 0.5 秒 +3. 写入 `False` (释放) +4. 验证状态 + +### 2. 避免死锁 +**问题:** PLC 在初始化过程中等待弹窗确认,而代码等待初始化完成 +**解决:** 在初始化等待循环中实时检测弹窗,一旦出现立即处理 + +### 3. 超时保护 +- 弹窗检测超时:30 秒(在 `_handle_material_search_dialog` 中) +- 初始化超时:120 秒(在 `func_pack_device_init_auto_start_combined` 中) + +### 4. PyModbus 3.x API 兼容 +所有 Modbus 操作使用 keyword arguments: +```python +# 读取 +client.read_coils(address=5176, count=1) + +# 写入 +client.write_coil(address=5184, value=True) +``` + +## 向后兼容性 + +### 保留的原有函数 +- `func_pack_device_init()` - 单独的初始化函数,不包含弹窗处理 +- 仍可在 YAML 中通过 `auto-func_pack_device_init` 调用 +- 用于不需要自动处理弹窗的场景 + +### 新增的功能 +- 在 `func_pack_device_init_auto_start_combined()` 中集成弹窗处理 +- 通过参数控制,默认行为与之前兼容(点击"否") + +## 验证测试 + +### 测试场景 + +#### 场景 1:默认参数(不启用物料搜寻) +```bash +# 调用时不传参数 +BatteryStation/auto-func_pack_device_init_auto_start_combined: {} +``` +**预期结果:** +- ✅ 检测到弹窗 +- ✅ 自动点击"否"按钮 +- ✅ 初始化完成并启动成功 + +#### 场景 2:启用物料搜寻 +```bash +# 设置 material_search_enable=true +BatteryStation/auto-func_pack_device_init_auto_start_combined: + goal: + material_search_enable: true +``` +**预期结果:** +- ✅ 检测到弹窗 +- ✅ 自动点击"是"按钮 +- ✅ 初始化完成并启动成功 + +### 实际测试结果 + +**测试时间:** 2026-01-13 21:32:43 +**测试参数:** `material_search_enable: false` +**测试结果:** ✅ 成功 + +**关键时间节点:** +- 21:33:05 - 检测到弹窗 +- 21:33:05 - 按下"否"按钮 +- 21:33:06 - 释放"否"按钮 +- 21:33:07 - 弹窗处理完成 +- 21:33:08 - 初始化状态完成 +- 21:33:18 - 整个流程完成 + +**总耗时:** 约 35 秒(包含初始化全过程) + +## 注意事项 + +1. **CSV 配置依赖** + - 确保 `coin_cell_assembly_b.csv` 包含 69-71 行的 coil 配置 + - 地址转换逻辑:`modbus_addr = (csv_addr // 10) * 8 + (csv_addr % 10)` + +2. **默认行为** + - 默认 `material_search_enable=false`,即不启用物料搜寻 + - 如需启用,必须显式设置为 `true` + +3. **日志级别** + - 弹窗检测过程中的 `waiting for init_cmd` 使用 DEBUG 级别 + - 关键操作(检测到弹窗、按钮操作)使用 INFO 级别 + +4. **原有函数保留** + - `func_pack_device_init()` 仍然可用,但不包含弹窗处理 + - 如果单独调用此函数,仍需手动处理弹窗 + +## 文件清单 + +### 修改的文件 +1. `d:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly\coin_cell_assembly.py` + - 新增 `_handle_material_search_dialog()` 方法 + - 修改 `func_pack_device_init_auto_start_combined()` 函数 + +2. `d:\UniLabdev\Uni-Lab-OS\unilabos\registry\devices\coin_cell_workstation.yaml` + - 更新 `auto-func_pack_device_init_auto_start_combined` 配置 + - 添加 `material_search_enable` 参数说明 + +3. `d:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly\coin_cell_assembly_b.csv` + - 第 69-71 行添加三个 coil 配置 + +### 创建的测试文件(已删除) +1. `test_material_search_dialog.py` - 物料搜寻弹窗测试脚本 +2. `test_coin_cell_reset.py` - 复位功能测试(更新为 pymodbus 3.x) +3. `test_unilab_interact.py` - Unilab 交互测试(更新为 pymodbus 3.x) + +## 总结 + +本次更新成功实现了设备初始化过程中物料搜寻确认弹窗的自动化处理,主要优势: + +✅ **全自动化** - 无需人工干预 +✅ **参数可配** - 灵活控制是否启用物料搜寻 +✅ **实时检测** - 在初始化等待循环中检测,避免死锁 +✅ **脉冲模式** - 模拟真实按钮操作 +✅ **向后兼容** - 保留原有函数,不影响现有流程 +✅ **完整日志** - 详细记录每一步操作 +✅ **超时保护** - 防止无限等待 + +该功能已通过实际测试验证,可投入生产使用。 + +--- + +**文档版本:** 1.0 +**创建日期:** 2026-01-13 +**作者:** Antigravity AI Assistant +**最后更新:** 2026-01-13 21:36 diff --git a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py new file mode 100644 index 00000000..c9187e65 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py @@ -0,0 +1,645 @@ +""" +纽扣电池组装工作站物料类定义 +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 + +from unilabos.resources.battery.magazine import MagazineHolder_4_Cathode, MagazineHolder_6_Cathode, MagazineHolder_6_Anode, MagazineHolder_6_Battery +from unilabos.resources.battery.bottle_carriers import YIHUA_Electrolyte_12VialCarrier +from unilabos.resources.battery.electrode_sheet import ElectrodeSheet + + + +# 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, + } + + +#是一种类型注解,不用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] + + +def TipBox64( + name: str, + size_x: float = 127.8, + size_y: float = 85.5, + size_z: float = 60.0, + category: str = "tip_rack", + model: Optional[str] = None, +): + """64孔枪头盒类""" + from pylabrobot.resources.tip import Tip + + # 创建12x8=96个枪头位 + 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=12, + 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, + ) + idx_available = list(range(0, 32)) + list(range(64, 96)) + tip_spots_available = {k: v for i, (k, v) in enumerate(tip_spots.items()) if i in idx_available} + tip_rack = TipRack( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + # ordered_items=tip_spots_available, + ordered_items=tip_spots, + category=category, + model=model, + with_tips=False, + ) + tip_rack.set_tip_state([True]*32 + [False]*32 + [True]*32) # 前32和后32个有枪头,中间32个无枪头 + return tip_rack + + +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, + material_z_thickness=0, + max_volume=float("inf"), + category="trash", + model=None, + compute_volume_from_height=None, + compute_height_from_volume=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 CoincellDeck(Deck): + """纽扣电池组装工作站台面类""" + + def __init__( + self, + name: str = "coin_cell_deck", + size_x: float = 1450.0, # 1m + size_y: float = 1450.0, # 1m + size_z: float = 100.0, # 0.9m + origin: Coordinate = Coordinate(-2200, 0, 0), + category: str = "coin_cell_deck", + setup: bool = False, # 是否自动执行 setup + ): + """初始化纽扣电池组装工作站台面 + + Args: + name: 台面名称 + size_x: 长度 (mm) - 1m + size_y: 宽度 (mm) - 1m + size_z: 高度 (mm) - 0.9m + origin: 原点坐标 + category: 类别 + setup: 是否自动执行 setup 配置标准布局 + """ + super().__init__( + name=name, + size_x=1450.0, + size_y=1450.0, + size_z=100.0, + origin=origin, + ) + if setup: + self.setup() + + def setup(self) -> None: + """设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置""" + # ====================================== 子弹夹 ============================================ + + # 正极片(4个洞位,2x2布局) + zhengji_zip = MagazineHolder_4_Cathode("正极&铝箔弹夹") + self.assign_child_resource(zhengji_zip, Coordinate(x=402.0, y=830.0, z=0)) + + # 正极壳、平垫片(6个洞位,2x2+2布局) + zhengjike_zip = MagazineHolder_6_Cathode("正极壳&平垫片弹夹") + self.assign_child_resource(zhengjike_zip, Coordinate(x=566.0, y=272.0, z=0)) + + # 负极壳、弹垫片(6个洞位,2x2+2布局) + fujike_zip = MagazineHolder_6_Anode("负极壳&弹垫片弹夹") + self.assign_child_resource(fujike_zip, Coordinate(x=474.0, y=276.0, z=0)) + + # 成品弹夹(6个洞位,3x2布局) + chengpindanjia_zip = MagazineHolder_6_Battery("成品弹夹") + self.assign_child_resource(chengpindanjia_zip, Coordinate(x=260.0, y=156.0, z=0)) + + # ====================================== 物料板 ============================================ + # 创建物料板(料盘carrier)- 4x4布局 + # 负极料盘 + fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True) + self.assign_child_resource(fujiliaopan, Coordinate(x=708.0, y=794.0, z=0)) + # for i in range(16): + # fujipian = ElectrodeSheet(name=f"{fujiliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + # fujiliaopan.children[i].assign_child_resource(fujipian, location=None) + + # 隔膜料盘 + gemoliaopan = MaterialPlate(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0, fill=True) + self.assign_child_resource(gemoliaopan, Coordinate(x=718.0, y=918.0, z=0)) + # for i in range(16): + # gemopian = ElectrodeSheet(name=f"{gemoliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + # gemoliaopan.children[i].assign_child_resource(gemopian, location=None) + + # ====================================== 瓶架、移液枪 ============================================ + # 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒 + # 奔耀上料5ml分液瓶小板 - 由奔曜跨站转运而来,不单独写,但是这里应该有一个堆栈用于摆放分液瓶小板 + + # bottle_rack_3x4 = BottleRack( + # name="bottle_rack_3x4", + # size_x=210.0, + # size_y=140.0, + # size_z=100.0, + # num_items_x=2, + # num_items_y=4, + # position_spacing=35.0, + # orientation="vertical", + # ) + # self.assign_child_resource(bottle_rack_3x4, Coordinate(x=1542.0, y=717.0, z=0)) + + # 电解液缓存位 - 6x2布局 + bottle_rack_6x2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2") + self.assign_child_resource(bottle_rack_6x2, Coordinate(x=1050.0, y=358.0, z=0)) + # 电解液回收位6x2 + bottle_rack_6x2_2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2_2") + self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=914.0, y=358.0, z=0)) + + tip_box = TipBox64(name="tip_box_64") + self.assign_child_resource(tip_box, Coordinate(x=782.0, y=514.0, z=0)) + + waste_tip_box = WasteTipBox(name="waste_tip_box") + self.assign_child_resource(waste_tip_box, Coordinate(x=778.0, y=622.0, z=0)) + + +def YH_Deck(name=""): + cd = CoincellDeck(name=name) + cd.setup() + return cd + + +if __name__ == "__main__": + deck = create_coin_cell_deck() + print(deck) \ No newline at end of file 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 f663a217..00000000 --- a/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py +++ /dev/null @@ -1,1289 +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 # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等) - 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 - ) - - 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): - """子弹夹洞位类""" - children: List[ElectrodeSheet] = [] - def __init__( - self, - name: str, - diameter: float, - depth: float, - category: str = "clip_magazine_hole", - ): - """初始化子弹夹洞位 - - Args: - name: 洞位名称 - diameter: 洞直径 (mm) - depth: 洞深度 (mm) - category: 类别 - """ - super().__init__( - name=name, - size_x=diameter, - size_y=diameter, - size_z=depth, - category=category, - ) - self.diameter = diameter - self.depth = depth - - def can_add_sheet(self, sheet: ElectrodeSheet) -> bool: - """检查是否可以添加极片 - - 根据洞的深度和极片的厚度来判断是否可以添加极片 - """ - # 检查极片直径是否适合洞的直径 - if sheet._unilabos_state["diameter"] > self.diameter: - return False - - # 计算当前已添加极片的总厚度 - current_thickness = sum(s._unilabos_state["thickness"] for s in self.children) - - # 检查添加新极片后总厚度是否超过洞的深度 - if current_thickness + sheet._unilabos_state["thickness"] > self.depth: - return False - - return True - - - def assign_child_resource( - self, - resource: ElectrodeSheet, - location: Optional[Coordinate] = None, - reassign: bool = True, - ): - """放置极片到洞位中 - - Args: - resource: 要放置的极片 - location: 极片在洞位中的位置(对于洞位,通常为None) - reassign: 是否允许重新分配 - """ - # 检查是否可以添加极片 - if not self.can_add_sheet(resource): - raise ValueError(f"无法向洞位 {self.name} 添加极片:直径或厚度不匹配") - - # 调用父类方法实际执行分配 - super().assign_child_resource(resource, location, reassign) - - def unassign_child_resource(self, resource: ElectrodeSheet): - """从洞位中移除极片 - - Args: - resource: 要移除的极片 - """ - if resource not in self.children: - raise ValueError(f"极片 {resource.name} 不在洞位 {self.name} 中") - - # 调用父类方法实际执行移除 - super().unassign_child_resource(resource) - - - - def serialize_state(self) -> Dict[str, Any]: - return { - "sheet_count": len(self.children), - "sheets": [sheet.serialize() for sheet in self.children], - } -class ClipMagazine_four(ItemizedResource[ClipMagazineHole]): - """子弹夹类 - 有4个洞位,每个洞位放多个极片""" - children: List[ClipMagazineHole] - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - hole_diameter: float = 14.0, - hole_depth: float = 10.0, - hole_spacing: float = 25.0, - max_sheets_per_hole: int = 100, - category: str = "clip_magazine_four", - 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: 型号 - """ - # 创建4个洞位,排成2x2布局 - holes = create_ordered_items_2d( - klass=ClipMagazineHole, - num_items_x=2, - 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=hole_diameter, - depth=hole_depth, - ) - - 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, - } -# TODO: 这个要改 -class ClipMagazine(ItemizedResource[ClipMagazineHole]): - """子弹夹类 - 有6个洞位,每个洞位放多个极片""" - children: List[ClipMagazineHole] - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - hole_diameter: float = 14.0, - hole_depth: float = 10.0, - 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=hole_diameter, - depth=hole_depth, - ) - - 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 - - electrolyte_name: str - electrolyte_volume: float - -class Battery(Resource): - """电池类 - 可容纳极片""" - children: List[ElectrodeSheet] = [] - - def __init__( - self, - name: str, - 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() - - 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, - ): - """放置极片""" - 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] - -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() - # 记录网格参数用于前端渲染 - self._grid_params = { - "num_items_x": 8, - "num_items_y": 8, - "dx": 8.0, - "dy": 8.0, - "item_dx": 9.0, - "item_dy": 9.0, - } - 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, - ) - - def serialize(self) -> dict: - return { - **super().serialize(), - **self._grid_params, - } - - - -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 BottleRackState(TypedDict): - """ bottle_diameter: 瓶子直径 (mm) - bottle_height: 瓶子高度 (mm) - position_spacing: 位置间距 (mm)""" - bottle_diameter: float - bottle_height: float - position_spacing: float - name_to_index: dict - - -class BottleRack(Resource): - """瓶架类 - 12个待配位置+12个已配位置""" - children: List[Resource] = [] - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - category: str = "bottle_rack", - model: Optional[str] = None, - num_items_x: int = 3, - num_items_y: int = 4, - position_spacing: float = 35.0, - orientation: str = "horizontal", - padding_x: float = 20.0, - padding_y: float = 20.0, - ): - """初始化瓶架 - - 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, - ) - # 初始化状态 - self._unilabos_state: BottleRackState = BottleRackState( - bottle_diameter=30.0, - bottle_height=100.0, - position_spacing=position_spacing, - name_to_index={}, - ) - # 基于网格生成瓶位坐标映射(居中摆放) - # 使用内边距,避免点跑到容器外(前端渲染不按mm等比缩放时更稳妥) - origin_x = padding_x - origin_y = padding_y - self.index_to_pos = {} - for j in range(num_items_y): - for i in range(num_items_x): - idx = j * num_items_x + i - if orientation == "vertical": - # 纵向:沿 y 方向优先排列 - self.index_to_pos[idx] = Coordinate( - x=origin_x + j * position_spacing, - y=origin_y + i * position_spacing, - z=0, - ) - else: - # 横向(默认):沿 x 方向优先排列 - self.index_to_pos[idx] = Coordinate( - x=origin_x + i * position_spacing, - y=origin_y + j * position_spacing, - z=0, - ) - self.name_to_index = {} - self.name_to_pos = {} - self.num_items_x = num_items_x - self.num_items_y = num_items_y - self.orientation = orientation - self.padding_x = padding_x - self.padding_y = padding_y - - 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_old(self, resource: Resource, location=Coordinate.zero(), reassign=True): - capacity = self.num_items_x * self.num_items_y - assert len(self.children) < capacity, "瓶架已满,无法添加更多瓶子" - index = len(self.children) - location = self.index_to_pos.get(index, Coordinate.zero()) - 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(self, resource: Resource, index: int): - capacity = self.num_items_x * self.num_items_y - assert 0 <= index < capacity, "无效的瓶子索引" - 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) -> dict: - return { - **super().serialize(), - "num_items_x": self.num_items_x, - "num_items_y": self.num_items_y, - "position_spacing": self._unilabos_state.get("position_spacing", 35.0), - "orientation": self.orientation, - "padding_x": self.padding_x, - "padding_y": self.padding_y, - } - - -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 - -def create_a_liaopan(): - liaopan = MaterialPlate(name="liaopan", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) - for i in range(16): - jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1) - liaopan1.children[i].assign_child_resource(jipian, location=None) - return liaopan - -def create_a_coin_cell_deck(): - deck = Deck(size_x=1200, - size_y=800, - 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"jipian_{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="liaopan3", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) - #把物料板放到桌子上 - deck.assign_child_resource(liaopan3, Coordinate(x=1000, y=0, z=0)) - - print(deck) - - return deck - - -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() - """======================================子弹夹============================================""" - zip_dan_jia = ClipMagazine_four("zi_dan_jia", 80, 80, 10) - deck.assign_child_resource(zip_dan_jia, Coordinate(x=1400, y=50, z=0)) - zip_dan_jia2 = ClipMagazine_four("zi_dan_jia2", 80, 80, 10) - deck.assign_child_resource(zip_dan_jia2, Coordinate(x=1600, y=200, z=0)) - zip_dan_jia3 = ClipMagazine("zi_dan_jia3", 80, 80, 10) - deck.assign_child_resource(zip_dan_jia3, Coordinate(x=1500, y=200, z=0)) - zip_dan_jia4 = ClipMagazine("zi_dan_jia4", 80, 80, 10) - deck.assign_child_resource(zip_dan_jia4, Coordinate(x=1500, y=300, z=0)) - zip_dan_jia5 = ClipMagazine("zi_dan_jia5", 80, 80, 10) - deck.assign_child_resource(zip_dan_jia5, Coordinate(x=1600, y=300, z=0)) - zip_dan_jia6 = ClipMagazine("zi_dan_jia6", 80, 80, 10) - deck.assign_child_resource(zip_dan_jia6, Coordinate(x=1530, y=500, z=0)) - zip_dan_jia7 = ClipMagazine("zi_dan_jia7", 80, 80, 10) - deck.assign_child_resource(zip_dan_jia7, Coordinate(x=1180, y=400, z=0)) - zip_dan_jia8 = ClipMagazine("zi_dan_jia8", 80, 80, 10) - deck.assign_child_resource(zip_dan_jia8, Coordinate(x=1280, y=400, z=0)) - for i in range(4): - jipian = ElectrodeSheet(name=f"zi_dan_jia_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia2.children[i].assign_child_resource(jipian, location=None) - for i in range(4): - jipian2 = ElectrodeSheet(name=f"zi_dan_jia2_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia.children[i].assign_child_resource(jipian2, location=None) - for i in range(6): - jipian3 = ElectrodeSheet(name=f"zi_dan_jia3_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia3.children[i].assign_child_resource(jipian3, location=None) - for i in range(6): - jipian4 = ElectrodeSheet(name=f"zi_dan_jia4_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia4.children[i].assign_child_resource(jipian4, location=None) - for i in range(6): - jipian5 = ElectrodeSheet(name=f"zi_dan_jia5_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia5.children[i].assign_child_resource(jipian5, location=None) - for i in range(6): - jipian6 = ElectrodeSheet(name=f"zi_dan_jia6_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia6.children[i].assign_child_resource(jipian6, location=None) - for i in range(6): - jipian7 = ElectrodeSheet(name=f"zi_dan_jia7_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia7.children[i].assign_child_resource(jipian7, location=None) - for i in range(6): - jipian8 = ElectrodeSheet(name=f"zi_dan_jia8_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia8.children[i].assign_child_resource(jipian8, location=None) - """======================================子弹夹============================================""" - #liaopan = TipBox64(name="liaopan") - """======================================物料板============================================""" - #创建一个4*4的物料板 - liaopan1 = MaterialPlate(name="liaopan1", size_x=120, size_y=100, size_z=10.0, fill=True) - deck.assign_child_resource(liaopan1, Coordinate(x=1010, y=50, z=0)) - for i in range(16): - jipian_1 = ElectrodeSheet(name=f"{liaopan1.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - liaopan1.children[i].assign_child_resource(jipian_1, location=None) - - liaopan2 = MaterialPlate(name="liaopan2", size_x=120, size_y=100, size_z=10.0, fill=True) - deck.assign_child_resource(liaopan2, Coordinate(x=1130, y=50, z=0)) - - liaopan3 = MaterialPlate(name="liaopan3", size_x=120, size_y=100, size_z=10.0, fill=True) - deck.assign_child_resource(liaopan3, Coordinate(x=1250, y=50, z=0)) - - liaopan4 = MaterialPlate(name="liaopan4", size_x=120, size_y=100, size_z=10.0, fill=True) - deck.assign_child_resource(liaopan4, Coordinate(x=1010, y=150, z=0)) - for i in range(16): - jipian_4 = ElectrodeSheet(name=f"{liaopan4.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - liaopan4.children[i].assign_child_resource(jipian_4, location=None) - liaopan5 = MaterialPlate(name="liaopan5", size_x=120, size_y=100, size_z=10.0, fill=True) - deck.assign_child_resource(liaopan5, Coordinate(x=1130, y=150, z=0)) - liaopan6 = MaterialPlate(name="liaopan6", size_x=120, size_y=100, size_z=10.0, fill=True) - deck.assign_child_resource(liaopan6, Coordinate(x=1250, y=150, z=0)) - #liaopan.children[3].assign_child_resource(jipian, location=None) - """======================================物料板============================================""" - """======================================瓶架,移液枪============================================""" - # 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒 - bottle_rack_3x4 = BottleRack( - name="bottle_rack_3x4", - size_x=210.0, - size_y=140.0, - size_z=100.0, - num_items_x=3, - num_items_y=4, - position_spacing=35.0, - orientation="vertical", - ) - deck.assign_child_resource(bottle_rack_3x4, Coordinate(x=100, y=200, z=0)) - - bottle_rack_6x2 = BottleRack( - name="bottle_rack_6x2", - size_x=120.0, - size_y=250.0, - size_z=100.0, - num_items_x=6, - num_items_y=2, - position_spacing=35.0, - orientation="vertical", - ) - deck.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0)) - - bottle_rack_6x2_2 = BottleRack( - name="bottle_rack_6x2_2", - size_x=120.0, - size_y=250.0, - size_z=100.0, - num_items_x=6, - num_items_y=2, - position_spacing=35.0, - orientation="vertical", - ) - deck.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=430, y=300, z=0)) - - - # 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位 - for idx in range(bottle_rack_3x4.num_items_x * bottle_rack_3x4.num_items_y): - sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1) - bottle_rack_3x4.assign_child_resource(sheet, index=idx) - - for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y): - sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1) - bottle_rack_6x2.assign_child_resource(sheet, index=idx) - - tip_box = TipBox64(name="tip_box_64") - deck.assign_child_resource(tip_box, Coordinate(x=300, y=100, z=0)) - - waste_tip_box = WasteTipBox(name="waste_tip_box") - deck.assign_child_resource(waste_tip_box, Coordinate(x=300, y=200, z=0)) - """======================================瓶架,移液枪============================================""" - print(deck) - - - from unilabos.resources.graphio import convert_resources_from_type - from unilabos.config.config import BasicConfig - BasicConfig.ak = "56bbed5b-6e30-438c-b06d-f69eaa63bb45" - BasicConfig.sk = "238222fe-0bf7-4350-a426-e5ced8011dcf" - from unilabos.app.web.client import http_client - - resources = convert_resources_from_type([deck], [Resource]) - - # 检查序列化后的资源 - - json.dump({"nodes": resources, "links": []}, open("button_battery_decks_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 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 4758bdda..91efd45f 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -1,44 +1,171 @@ import csv +import inspect import json import os import threading import time +import types from datetime import datetime from typing import Any, Dict, Optional -from pylabrobot.resources import Resource as PLRResource +from functools import wraps +from pylabrobot.resources import Deck, Resource as PLRResource from unilabos_msgs.msg import Resource from unilabos.device_comms.modbus_plc.client import ModbusTcpClient -from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import MaterialHole, MaterialPlate from unilabos.devices.workstation.workstation_base import WorkstationBase from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder -from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import * +from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import * from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode 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 +import struct + + +def _decode_float32_correct(registers): + """ + 正确解码FLOAT32类型的Modbus寄存器 + + Args: + registers: 从Modbus读取的原始寄存器值列表 + + Returns: + 正确解码的浮点数值 + + Note: + 根据test_glove_box_pressure.py验证的配置: + - Byte Order: Big (Modbus标准) + - Word Order: Little (PLC配置) + """ + if not registers or len(registers) < 2: + return 0.0 + + try: + # 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 + + +def _ensure_modbus_slave_kw_alias(modbus_client): + if modbus_client is None: + return + + method_names = [ + "read_coils", + "write_coils", + "write_coil", + "read_discrete_inputs", + "read_holding_registers", + "write_register", + "write_registers", + ] + + def _wrap(func): + signature = inspect.signature(func) + has_var_kwargs = any(param.kind == param.VAR_KEYWORD for param in signature.parameters.values()) + accepts_unit = has_var_kwargs or "unit" in signature.parameters + accepts_slave = has_var_kwargs or "slave" in signature.parameters + + @wraps(func) + def _wrapped(self, *args, **kwargs): + if "slave" in kwargs and not accepts_slave: + slave_value = kwargs.pop("slave") + if accepts_unit and "unit" not in kwargs: + kwargs["unit"] = slave_value + if "unit" in kwargs and not accepts_unit: + unit_value = kwargs.pop("unit") + if accepts_slave and "slave" not in kwargs: + kwargs["slave"] = unit_value + return func(self, *args, **kwargs) + + _wrapped._has_slave_alias = True + return _wrapped + + for name in method_names: + if not hasattr(modbus_client, name): + continue + bound_method = getattr(modbus_client, name) + func = getattr(bound_method, "__func__", None) + if func is None: + continue + if getattr(func, "_has_slave_alias", False): + continue + wrapped = _wrap(func) + setattr(modbus_client, name, types.MethodType(wrapped, modbus_client)) + + +def _coerce_deck_input(deck: Any) -> Optional[Deck]: + if deck is None: + return None + + if isinstance(deck, Deck): + return deck + + if isinstance(deck, PLRResource): + return deck if isinstance(deck, Deck) else None + + candidates = None + if isinstance(deck, dict): + if "nodes" in deck and isinstance(deck["nodes"], list): + candidates = deck["nodes"] + else: + candidates = [deck] + elif isinstance(deck, list): + candidates = deck + + if candidates is None: + return None + + try: + converted = convert_resources_to_type(resources_list=candidates, resource_type=Deck) + if isinstance(converted, Deck): + return converted + if isinstance(converted, list): + for item in converted: + if isinstance(item, Deck): + return item + except Exception as exc: + logger.warning(f"deck 转换 Deck 失败: {exc}") + return None + #构建物料系统 class CoinCellAssemblyWorkstation(WorkstationBase): - def __init__( - self, - deck: CoincellDeck, - address: str = "192.168.1.20", + def __init__(self, + config: dict = None, + deck=None, + address: str = "172.16.28.102", port: str = "502", - debug_mode: bool = True, + debug_mode: bool = False, *args, - **kwargs, - ): - super().__init__( - #桌子 - deck=deck, - *args, - **kwargs, - ) + **kwargs): + + if deck is None and config: + deck = config.get('deck') + if deck is None: + logger.info("没有传入依华deck,检查启动json文件") + super().__init__(deck=deck, *args, **kwargs,) self.debug_mode = debug_mode - self.deck = deck + """ 连接初始化 """ modbus_client = TCPClient(addr=address, port=port) - print("modbus_client", modbus_client) + logger.debug(f"创建 Modbus 客户端: {modbus_client}") + _ensure_modbus_slave_kw_alias(modbus_client.client) if not debug_mode: modbus_client.client.connect() count = 100 @@ -49,27 +176,21 @@ def __init__( time.sleep(2) if not modbus_client.client.is_socket_open(): raise ValueError('modbus tcp connection failed') + self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_b.csv')) + self.client = modbus_client.register_node_list(self.nodes) else: print("测试模式,跳过连接") + self.nodes, self.client = None, None """ 工站的配置 """ - self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv')) - self.client = modbus_client.register_node_list(self.nodes) + self.success = False self.allow_data_read = False #允许读取函数运行标志位 self.csv_export_thread = None self.csv_export_running = False self.csv_export_file = None - #创建一个物料台面,包含两个极片板 - #self.deck = create_a_coin_cell_deck() - - #self._ros_node.update_resource(self.deck) - - #ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ - # "resources": [self.deck] - #}) + self.coin_num_N = 0 #已组装电池数量 - def post_init(self, ros_node: ROS2WorkstationNode): self._ros_node = ros_node #self.deck = create_a_coin_cell_deck() @@ -418,48 +539,72 @@ def data_assembly_time(self) -> float: """单颗电池组装时间 (秒, REAL/FLOAT32)""" if self.debug_mode: return 0 - time, read_err = self.client.use_node('REG_DATA_ASSEMBLY_PER_TIME').read(2, word_order=WorderOrder.LITTLE) - return time + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_ASSEMBLY_PER_TIME').address, count=2) + if result.isError(): + logger.error(f"读取组装时间失败") + return 0.0 + return _decode_float32_correct(result.registers) @property def data_open_circuit_voltage(self) -> float: """开路电压值 (FLOAT32)""" if self.debug_mode: return 0 - vol, read_err = self.client.use_node('REG_DATA_OPEN_CIRCUIT_VOLTAGE').read(2, word_order=WorderOrder.LITTLE) - return vol + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_OPEN_CIRCUIT_VOLTAGE').address, count=2) + if result.isError(): + logger.error(f"读取开路电压失败") + return 0.0 + return _decode_float32_correct(result.registers) @property def data_axis_x_pos(self) -> float: """分液X轴当前位置 (FLOAT32)""" if self.debug_mode: return 0 - pos, read_err = self.client.use_node('REG_DATA_AXIS_X_POS').read(2, word_order=WorderOrder.LITTLE) - return pos + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_AXIS_X_POS').address, count=2) + if result.isError(): + logger.error(f"读取X轴位置失败") + return 0.0 + return _decode_float32_correct(result.registers) @property def data_axis_y_pos(self) -> float: """分液Y轴当前位置 (FLOAT32)""" if self.debug_mode: return 0 - pos, read_err = self.client.use_node('REG_DATA_AXIS_Y_POS').read(2, word_order=WorderOrder.LITTLE) - return pos + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_AXIS_Y_POS').address, count=2) + if result.isError(): + logger.error(f"读变Y轴位置失败") + return 0.0 + return _decode_float32_correct(result.registers) @property def data_axis_z_pos(self) -> float: """分液Z轴当前位置 (FLOAT32)""" if self.debug_mode: return 0 - pos, read_err = self.client.use_node('REG_DATA_AXIS_Z_POS').read(2, word_order=WorderOrder.LITTLE) - return pos + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_AXIS_Z_POS').address, count=2) + if result.isError(): + logger.error(f"读取Z轴位置失败") + return 0.0 + return _decode_float32_correct(result.registers) @property def data_pole_weight(self) -> float: """当前电池正极片称重数据 (FLOAT32)""" if self.debug_mode: return 0 - weight, read_err = self.client.use_node('REG_DATA_POLE_WEIGHT').read(2, word_order=WorderOrder.LITTLE) - return weight + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_POLE_WEIGHT').address, count=2) + if result.isError(): + logger.error(f"读取极片质量失败") + return 0.0 + return _decode_float32_correct(result.registers) @property def data_assembly_pressure(self) -> int: @@ -489,52 +634,97 @@ def data_coin_num(self) -> int: def data_coin_cell_code(self) -> str: """电池二维码序列号 (STRING)""" try: - # 尝试不同的字节序读取 + # 读取 STRING 类型数据 code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE) - print(code_little) - clean_code = code_little[-8:][::-1] - return clean_code + + # PyModbus 3.x 返回 string 类型 + if not isinstance(code_little, str): + logger.warning(f"电池二维码返回的类型不支持: {type(code_little)}, 值: {repr(code_little)}") + return "N/A" + + # 从字符串末尾查找连续的字母数字字符(反转字符串) + import re + reversed_str = code_little[::-1] + match = re.match(r'^([A-Za-z0-9]+)', reversed_str) + + if not match: + logger.warning(f"未找到有效的电池二维码数据,原始字符串: {repr(code_little)}") + return "N/A" + + # 提取匹配到的字符串(已经是正确顺序) + decoded = match.group(1)[:8] # 只取前8个字符 + + return decoded if decoded else "N/A" except Exception as e: - print(f"读取电池二维码失败: {e}") + logger.error(f"读取电池二维码失败: {e}") return "N/A" @property def data_electrolyte_code(self) -> str: + """电解液二维码序列号 (STRING)""" try: - # 尝试不同的字节序读取 + # 读取 STRING 类型数据 code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE) - print(code_little) - clean_code = code_little[-8:][::-1] - return clean_code + + # PyModbus 3.x 返回 string 类型 + if not isinstance(code_little, str): + logger.warning(f"电解液二维码返回的类型不支持: {type(code_little)}, 值: {repr(code_little)}") + return "N/A" + + # 从字符串末尾查找连续的字母数字字符(反转字符串) + import re + reversed_str = code_little[::-1] + match = re.match(r'^([A-Za-z0-9]+)', reversed_str) + + if not match: + logger.warning(f"未找到有效的电解液二维码数据,原始字符串: {repr(code_little)}") + return "N/A" + + # 提取匹配到的字符串(已经是正确顺序) + decoded = match.group(1)[:8] # 只取前8个字符 + + return decoded if decoded else "N/A" except Exception as e: - print(f"读取电解液二维码失败: {e}") + logger.error(f"读取电解液二维码失败: {e}") return "N/A" # ===================== 环境监控区 ====================== @property def data_glove_box_pressure(self) -> float: - """手套箱压力 (bar, FLOAT32)""" + """手套箱压力 (mbar, FLOAT32)""" if self.debug_mode: return 0 - status, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_PRESSURE').read(2, word_order=WorderOrder.LITTLE) - return status + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_GLOVE_BOX_PRESSURE').address, count=2) + if result.isError(): + logger.error(f"读取手套箱压力失败") + return 0.0 + return _decode_float32_correct(result.registers) @property def data_glove_box_o2_content(self) -> float: """手套箱氧含量 (ppm, FLOAT32)""" if self.debug_mode: return 0 - value, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_O2_CONTENT').read(2, word_order=WorderOrder.LITTLE) - return value + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_GLOVE_BOX_O2_CONTENT').address, count=2) + if result.isError(): + logger.error(f"读取手套箱氧含量失败") + return 0.0 + return _decode_float32_correct(result.registers) @property def data_glove_box_water_content(self) -> float: """手套箱水含量 (ppm, FLOAT32)""" if self.debug_mode: return 0 - value, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_WATER_CONTENT').read(2, word_order=WorderOrder.LITTLE) - return value + # 读取原始寄存器并正确解码FLOAT32 + result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_GLOVE_BOX_WATER_CONTENT').address, count=2) + if result.isError(): + logger.error(f"读取手套箱水含量失败") + return 0.0 + return _decode_float32_correct(result.registers) # @property # def data_stack_vision_code(self) -> int: @@ -606,7 +796,326 @@ def func_pack_device_start(self): print("waiting for start_cmd") time.sleep(1) - def func_pack_send_bottle_num(self, bottle_num: int): + def _handle_material_search_dialog(self, enable_search: bool, timeout: int = 30) -> None: + """处理物料搜寻确认弹窗 + + 监测弹窗是否出现,并根据参数自动点击"是"或"否"按钮(脉冲模式) + + Args: + enable_search: True=点击"是"启用物料搜寻, False=点击"否"不启用物料搜寻 + timeout: 等待弹窗出现的最大时间(秒),默认30秒 + + Raises: + RuntimeError: 超时未检测到弹窗,或操作失败 + """ + logger.info(f"开始监测物料搜寻确认弹窗(超时: {timeout}秒)...") + logger.info(f"用户选择: {'启用物料搜寻(点击是)' if enable_search else '不启用物料搜寻(点击否)'}") + + start_time = time.time() + dialog_appeared = False + + # 步骤1: 监测弹窗是否出现 + while time.time() - start_time < timeout: + try: + dialog_node = self.client.use_node('COIL_MATERIAL_SEARCH_DIALOG_APPEAR') + dialog_state, read_err = dialog_node.read(1) + + if read_err: + logger.warning("读取弹窗状态时出错,继续监测...") + time.sleep(0.5) + continue + + # 提取实际值 + if isinstance(dialog_state, (list, tuple)): + dialog_actual = dialog_state[0] if len(dialog_state) > 0 else False + else: + dialog_actual = dialog_state + + if dialog_actual: + logger.info("✓ 检测到物料搜寻确认弹窗出现!") + dialog_appeared = True + break + + except Exception as e: + logger.warning(f"读取弹窗状态异常: {e},继续监测...") + + time.sleep(0.5) # 每0.5秒检查一次 + + if not dialog_appeared: + error_msg = f"❌ 超时未检测到物料搜寻确认弹窗(等待了 {timeout} 秒)" + logger.error(error_msg) + raise RuntimeError(error_msg) + + # 步骤2: 执行脉冲按钮点击 + button_name = "是" if enable_search else "否" + coil_name = "COIL_MATERIAL_SEARCH_CONFIRM_YES" if enable_search else "COIL_MATERIAL_SEARCH_CONFIRM_NO" + + logger.info(f"执行脉冲按钮点击: '{button_name}'") + + try: + button_node = self.client.use_node(coil_name) + + # 读取初始状态 + initial_state, _ = button_node.read(1) + logger.debug(f"按钮'{button_name}'初始状态: {initial_state}") + + # 脉冲步骤1: 设置为 True + logger.info(f" → 按下按钮 '{button_name}' (设置为 True)") + button_node.write(True) + time.sleep(0.5) # 保持0.5秒 + + # 验证已按下 + pressed_state, _ = button_node.read(1) + if isinstance(pressed_state, (list, tuple)): + pressed_actual = pressed_state[0] if len(pressed_state) > 0 else False + else: + pressed_actual = pressed_state + + if pressed_actual: + logger.info(f" ✓ 按钮 '{button_name}' 已按下") + else: + logger.warning(f" ⚠ 按钮 '{button_name}' 状态未变为 True,当前值: {pressed_actual}") + + # 脉冲步骤2: 释放按钮 (设置为 False) + logger.info(f" → 释放按钮 '{button_name}' (设置为 False)") + button_node.write(False) + time.sleep(0.3) + + # 验证已释放 + released_state, _ = button_node.read(1) + if isinstance(released_state, (list, tuple)): + released_actual = released_state[0] if len(released_state) > 0 else False + else: + released_actual = released_state + + if not released_actual: + logger.info(f" ✓ 按钮 '{button_name}' 已释放(脉冲完成)") + else: + logger.warning(f" ⚠ 按钮 '{button_name}' 未正确释放,当前值: {released_actual}") + + logger.info(f"✓ 成功处理物料搜寻确认弹窗(选择: {button_name})") + + except Exception as e: + error_msg = f"❌ 执行按钮'{button_name}'脉冲操作时失败: {e}" + logger.error(error_msg) + raise RuntimeError(error_msg) + + + def func_pack_device_init_auto_start_combined(self, material_search_enable: bool = False) -> bool: + """ + 组合函数:设备初始化 + 切换自动模式 + 启动 + + 整合了原有的三个独立函数: + 1. func_pack_device_init() - 设备初始化 + 2. func_pack_device_auto() - 切换自动模式 + 3. func_pack_device_start() - 启动设备 + + Args: + material_search_enable: 是否启用物料搜寻功能。 + 设备初始化后会弹出物料搜寻确认弹窗, + 此参数控制自动点击'是'(启用)或'否'(不启用)。 + 默认为False(不启用物料搜寻)。 + + Returns: + bool: 操作成功返回 True,失败返回 False + """ + logger.info("=" * 60) + logger.info("开始组合操作:设备初始化 → 物料搜寻确认 → 自动模式 → 启动") + logger.info("=" * 60) + + # 步骤0: 前置条件检查 + logger.info("\n【步骤 0/4】前置条件检查...") + try: + # 检查 REG_UNILAB_INTERACT (应该为False,表示使用Unilab交互) + unilab_interact_node = self.client.use_node('REG_UNILAB_INTERACT') + unilab_interact_value, read_err = unilab_interact_node.read(1) + + if read_err: + error_msg = "❌ 无法读取 REG_UNILAB_INTERACT 状态!请检查设备连接。" + logger.error(error_msg) + raise RuntimeError(error_msg) + + # 提取实际值(处理可能的列表或单值) + if isinstance(unilab_interact_value, (list, tuple)): + unilab_interact_actual = unilab_interact_value[0] if len(unilab_interact_value) > 0 else None + else: + unilab_interact_actual = unilab_interact_value + + logger.info(f" REG_UNILAB_INTERACT 当前值: {unilab_interact_actual}") + + if unilab_interact_actual != False: + error_msg = ( + "❌ 前置条件检查失败!\n" + f" REG_UNILAB_INTERACT = {unilab_interact_actual} (期望值: False)\n" + " 说明: 当前设备设置为'忽略Unilab交互'模式\n" + " 操作: 请在HMI上确认并切换为'使用Unilab交互'模式\n" + " 提示: REG_UNILAB_INTERACT应该为False才能继续" + ) + logger.error(error_msg) + raise RuntimeError(error_msg) + + logger.info(" ✓ REG_UNILAB_INTERACT 检查通过 (值为False,使用Unilab交互)") + + # 检查 COIL_GB_L_IGNORE_CMD (应该为False,表示使用左手套箱) + gb_l_ignore_node = self.client.use_node('COIL_GB_L_IGNORE_CMD') + gb_l_ignore_value, read_err = gb_l_ignore_node.read(1) + + if read_err: + error_msg = "❌ 无法读取 COIL_GB_L_IGNORE_CMD 状态!请检查设备连接。" + logger.error(error_msg) + raise RuntimeError(error_msg) + + # 提取实际值 + if isinstance(gb_l_ignore_value, (list, tuple)): + gb_l_ignore_actual = gb_l_ignore_value[0] if len(gb_l_ignore_value) > 0 else None + else: + gb_l_ignore_actual = gb_l_ignore_value + + logger.info(f" COIL_GB_L_IGNORE_CMD 当前值: {gb_l_ignore_actual}") + + if gb_l_ignore_actual != False: + error_msg = ( + "❌ 前置条件检查失败!\n" + f" COIL_GB_L_IGNORE_CMD = {gb_l_ignore_actual} (期望值: False)\n" + " 说明: 当前设备设置为'忽略左手套箱'模式\n" + " 操作: 请在HMI上确认并切换为'使用左手套箱'模式\n" + " 提示: COIL_GB_L_IGNORE_CMD应该为False才能继续" + ) + logger.error(error_msg) + raise RuntimeError(error_msg) + + logger.info(" ✓ COIL_GB_L_IGNORE_CMD 检查通过 (值为False,使用左手套箱)") + logger.info("✓ 所有前置条件检查通过!") + + except ValueError as e: + # 节点未找到 + error_msg = f"❌ 配置错误:{str(e)}\n请检查CSV配置文件是否包含必要的节点。" + logger.error(error_msg) + raise RuntimeError(error_msg) + except Exception as e: + # 其他异常 + error_msg = f"❌ 前置条件检查异常:{str(e)}" + logger.error(error_msg) + raise + + # 步骤1: 设备初始化(包含弹窗检测) + logger.info("\n【步骤 1/4】设备初始化...") + try: + # 切换手动模式 + logger.info("切换手动模式...") + self._sys_hand_cmd(True) + time.sleep(1) + while (self._sys_hand_status()) == False: + logger.debug("waiting for hand_cmd") + time.sleep(1) + + # 设备初始化命令 + logger.info("发送初始化命令...") + self._sys_init_cmd(True) + time.sleep(1) + + # 等待初始化完成,同时检测物料搜寻弹窗 + logger.info("等待初始化完成(同时监测物料搜寻弹窗)...") + dialog_handled = False + max_wait_time = 120 # 最多等待120秒 + start_wait = time.time() + + while (self._sys_init_status()) == False: + # 检查是否超时 + if time.time() - start_wait > max_wait_time: + raise RuntimeError(f"初始化超时(超过 {max_wait_time} 秒)") + + # 如果还没处理弹窗,检测弹窗是否出现 + if not dialog_handled: + try: + dialog_node = self.client.use_node('COIL_MATERIAL_SEARCH_DIALOG_APPEAR') + dialog_state, read_err = dialog_node.read(1) + + if not read_err: + # 提取实际值 + if isinstance(dialog_state, (list, tuple)): + dialog_actual = dialog_state[0] if len(dialog_state) > 0 else False + else: + dialog_actual = dialog_state + + # 如果弹窗出现,立即处理 + if dialog_actual: + logger.info("✓ 在初始化过程中检测到物料搜寻确认弹窗!") + logger.info(f"用户选择: {'启用物料搜寻(点击是)' if material_search_enable else '不启用物料搜寻(点击否)'}") + + # 执行脉冲按钮点击 + button_name = "是" if material_search_enable else "否" + coil_name = "COIL_MATERIAL_SEARCH_CONFIRM_YES" if material_search_enable else "COIL_MATERIAL_SEARCH_CONFIRM_NO" + + button_node = self.client.use_node(coil_name) + + # 脉冲:True -> 等待 -> False + logger.info(f" → 按下按钮 '{button_name}'") + button_node.write(True) + time.sleep(0.5) + logger.info(f" → 释放按钮 '{button_name}'") + button_node.write(False) + time.sleep(0.3) + + logger.info(f"✓ 成功处理物料搜寻确认弹窗(选择: {button_name})") + dialog_handled = True + + except Exception as e: + logger.debug(f"检测弹窗时出错: {e}") + + logger.debug("waiting for init_cmd") + time.sleep(1) + + logger.info("✓ 初始化状态完成") + + # 手动按钮置回False + self._sys_hand_cmd(False) + time.sleep(1) + while (self._sys_hand_cmd()) == True: + logger.debug("waiting for hand_cmd to False") + time.sleep(1) + + # 初始化命令置回False + self._sys_init_cmd(False) + time.sleep(1) + while (self._sys_init_cmd()) == True: + logger.debug("waiting for init_cmd to False") + time.sleep(1) + + logger.info("✓ 设备初始化完成") + except Exception as e: + logger.error(f"❌ 设备初始化失败: {e}") + return False + + # 步骤1.5已经在步骤1中处理,跳过 + logger.info("\n【步骤 1.5/4】物料搜寻确认已在初始化过程中完成") + + # 步骤2: 切换自动模式 + logger.info("\n【步骤 2/4】切换自动模式...") + try: + self.func_pack_device_auto() + logger.info("✓ 切换自动模式完成") + except Exception as e: + logger.error(f"❌ 切换自动模式失败: {e}") + return False + + # 步骤3: 启动设备 + logger.info("\n【步骤 3/4】启动设备...") + try: + self.func_pack_device_start() + logger.info("✓ 启动设备完成") + except Exception as e: + logger.error(f"❌ 启动设备失败: {e}") + return False + + logger.info("\n" + "=" * 60) + logger.info("组合操作完成:设备已成功初始化、确认物料搜寻、切换自动模式并启动") + logger.info("=" * 60) + + return True + + def func_pack_send_bottle_num(self, bottle_num): + bottle_num = int(bottle_num) #发送电解液平台数 print("启动") while (self._unilab_rece_electrolyte_bottle_num()) == False: @@ -628,6 +1137,111 @@ def func_pack_send_bottle_num(self, bottle_num: int): 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: @@ -654,16 +1268,25 @@ def func_pack_send_bottle_num(self, bottle_num: int): # self.success = True # return self.success - def func_pack_send_msg_cmd(self, elec_use_num) -> bool: + def func_pack_send_msg_cmd(self, elec_use_num, elec_vol, assembly_type, assembly_pressure) -> bool: """UNILAB写参数""" while (self.request_rec_msg_status) == False: print("wait for request_rec_msg_status to True") time.sleep(1) self.success = False #self._unilab_send_msg_electrolyte_num(elec_num) - time.sleep(1) + #设置平行样数目 self._unilab_send_msg_electrolyte_use_num(elec_use_num) time.sleep(1) + #发送电解液加注量 + self._unilab_send_msg_electrolyte_vol(elec_vol) + time.sleep(1) + #发送电解液组装类型 + self._unilab_send_msg_assembly_type(assembly_type) + time.sleep(1) + #发送电池压制力 + self._unilab_send_msg_assembly_pressure(assembly_pressure) + time.sleep(1) self._unilab_send_msg_succ_cmd(True) time.sleep(1) while (self.request_rec_msg_status) == True: @@ -680,23 +1303,111 @@ def func_pack_get_msg_cmd(self, file_path: str="D:\\coin_cell_data") -> bool: while self.request_send_msg_status == False: print("waiting for send_read_msg_status to True") time.sleep(1) - data_open_circuit_voltage = self.data_open_circuit_voltage - data_pole_weight = self.data_pole_weight + + # 处理开路电压 - 确保是数值类型 + try: + data_open_circuit_voltage = self.data_open_circuit_voltage + if isinstance(data_open_circuit_voltage, (list, tuple)) and len(data_open_circuit_voltage) > 0: + data_open_circuit_voltage = float(data_open_circuit_voltage[0]) + else: + data_open_circuit_voltage = float(data_open_circuit_voltage) + except Exception as e: + print(f"读取开路电压失败: {e}") + logger.error(f"读取开路电压失败: {e}") + data_open_circuit_voltage = 0.0 + + # 处理极片质量 - 确保是数值类型 + try: + data_pole_weight = self.data_pole_weight + if isinstance(data_pole_weight, (list, tuple)) and len(data_pole_weight) > 0: + data_pole_weight = float(data_pole_weight[0]) + else: + data_pole_weight = float(data_pole_weight) + except Exception as e: + print(f"读取正极片重量失败: {e}") + logger.error(f"读取正极片重量失败: {e}") + data_pole_weight = 0.0 + data_assembly_time = self.data_assembly_time data_assembly_pressure = self.data_assembly_pressure data_electrolyte_volume = self.data_electrolyte_volume data_coin_num = self.data_coin_num - data_electrolyte_code = self.data_electrolyte_code - data_coin_cell_code = self.data_coin_cell_code - print("data_open_circuit_voltage", data_open_circuit_voltage) - print("data_pole_weight", data_pole_weight) - print("data_assembly_time", data_assembly_time) - print("data_assembly_pressure", data_assembly_pressure) - print("data_electrolyte_volume", data_electrolyte_volume) - print("data_coin_num", data_coin_num) - print("data_electrolyte_code", data_electrolyte_code) - print("data_coin_cell_code", data_coin_cell_code) + + # 处理电解液二维码 - 确保是字符串类型 + try: + data_electrolyte_code = self.data_electrolyte_code + if isinstance(data_electrolyte_code, str): + data_electrolyte_code = data_electrolyte_code.strip() + else: + data_electrolyte_code = str(data_electrolyte_code) + except Exception as e: + print(f"读取电解液二维码失败: {e}") + logger.error(f"读取电解液二维码失败: {e}") + data_electrolyte_code = "N/A" + + # 处理电池二维码 - 确保是字符串类型 + try: + data_coin_cell_code = self.data_coin_cell_code + if isinstance(data_coin_cell_code, str): + data_coin_cell_code = data_coin_cell_code.strip() + else: + data_coin_cell_code = str(data_coin_cell_code) + except Exception as e: + print(f"读取电池二维码失败: {e}") + logger.error(f"读取电池二维码失败: {e}") + data_coin_cell_code = "N/A" + logger.debug(f"data_open_circuit_voltage: {data_open_circuit_voltage}") + logger.debug(f"data_pole_weight: {data_pole_weight}") + logger.debug(f"data_assembly_time: {data_assembly_time}") + logger.debug(f"data_assembly_pressure: {data_assembly_pressure}") + logger.debug(f"data_electrolyte_volume: {data_electrolyte_volume}") + logger.debug(f"data_coin_num: {data_coin_num}") + logger.debug(f"data_electrolyte_code: {data_electrolyte_code}") + logger.debug(f"data_coin_cell_code: {data_coin_cell_code}") #接收完信息后,读取完毕标志位置True + liaopan3 = self.deck.get_resource("成品弹夹") + + # 生成唯一的电池名称(使用时间戳确保唯一性) + timestamp_suffix = datetime.now().strftime("%Y%m%d_%H%M%S_%f") + battery_name = f"battery_{self.coin_num_N}_{timestamp_suffix}" + + # 检查目标位置是否已有资源,如果有则先卸载 + target_slot = liaopan3.children[self.coin_num_N] + if target_slot.children: + logger.warning(f"位置 {self.coin_num_N} 已有资源,将先卸载旧资源") + try: + # 卸载所有现有子资源 + for child in list(target_slot.children): + target_slot.unassign_child_resource(child) + logger.info(f"已卸载旧资源: {child.name}") + except Exception as e: + logger.error(f"卸载旧资源时出错: {e}") + + # 创建新的电池资源 + battery = ElectrodeSheet(name=battery_name, size_x=14, size_y=14, size_z=2) + battery._unilabos_state = { + "electrolyte_name": data_coin_cell_code, + "data_electrolyte_code": data_electrolyte_code, + "open_circuit_voltage": data_open_circuit_voltage, + "assembly_pressure": data_assembly_pressure, + "electrolyte_volume": data_electrolyte_volume + } + + # 分配新资源到目标位置 + try: + target_slot.assign_child_resource(battery, location=None) + logger.info(f"成功分配电池 {battery_name} 到位置 {self.coin_num_N}") + except Exception as e: + logger.error(f"分配电池资源失败: {e}") + # 如果分配失败,尝试使用更简单的方法 + raise + + #print(jipian2.parent) + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [self.deck] + }) + + self._unilab_rec_msg_succ_cmd(True) time.sleep(1) #等待允许读取标志位置False @@ -754,10 +1465,27 @@ def func_pack_send_finished_cmd(self) -> bool: self.success = True return self.success + def qiming_coin_cell_code(self, fujipian_panshu:int, fujipian_juzhendianwei:int=0, gemopanshu:int=0, gemo_juzhendianwei:int=0, lvbodian:bool=True, battery_pressure_mode:bool=True, battery_pressure:int=4000, battery_clean_ignore:bool=False) -> bool: + self.success = False + self.client.use_node('REG_MSG_NE_PLATE_NUM').write(fujipian_panshu) + self.client.use_node('REG_MSG_NE_PLATE_MATRIX').write(fujipian_juzhendianwei) + self.client.use_node('REG_MSG_SEPARATOR_PLATE_NUM').write(gemopanshu) + self.client.use_node('REG_MSG_SEPARATOR_PLATE_MATRIX').write(gemo_juzhendianwei) + self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian) + self.client.use_node('REG_MSG_PRESS_MODE').write(not battery_pressure_mode) + # self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(battery_pressure) + self.client.use_node('REG_MSG_BATTERY_CLEAN_IGNORE').write(battery_clean_ignore) + self.success = True + + return self.success - - def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell_data") -> bool: + def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="/Users/sml/work") -> Dict[str, Any]: + elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure) summary_csv_file = os.path.join(file_path, "duandian.csv") + + # 用于收集所有电池的数据 + battery_data_list = [] + # 如果断点文件存在,先读取之前的进度 if os.path.exists(summary_csv_file): read_status_flag = True @@ -775,7 +1503,12 @@ def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell print("断点文件与当前任务匹配,继续") else: print("断点文件中elec_num、elec_use_num与当前任务不匹配,请检查任务下发参数或修改断点文件") - return False + return { + "success": False, + "error": "断点文件参数不匹配", + "total_batteries": 0, + "batteries": [] + } print(f"从断点文件读取进度: elec_num_N={elec_num_N}, elec_use_num_N={elec_use_num_N}, coin_num_N={coin_num_N}") else: @@ -784,54 +1517,88 @@ def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell elec_num_N = 0 elec_use_num_N = 0 coin_num_N = 0 - - print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}") - + for i in range(20): + print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}") + print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}") + print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}") #如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。 if read_status_flag == False: + pass #初始化 - self.func_pack_device_init() + #self.func_pack_device_init() #切换自动 - self.func_pack_device_auto() + #self.func_pack_device_auto() #启动,小车收回 - self.func_pack_device_start() + #self.func_pack_device_start() #发送电解液瓶数量,启动搬运,多搬运没事 - self.func_pack_send_bottle_num(elec_num) + #self.func_pack_send_bottle_num(elec_num) last_i = elec_num_N last_j = elec_use_num_N for i in range(last_i, elec_num): print(f"开始第{last_i+i+1}瓶电解液的组装") #第一个循环从上次断点继续,后续循环从0开始 j_start = last_j if i == last_i else 0 - self.func_pack_send_msg_cmd(elec_use_num-j_start) + self.func_pack_send_msg_cmd(elec_use_num-j_start, elec_vol, assembly_type, assembly_pressure) for j in range(j_start, elec_use_num): print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装") + #读取电池组装数据并存入csv self.func_pack_get_msg_cmd(file_path) - time.sleep(1) - - #这里定义物料系统 - # TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑 - liaopan1 = self.deck.get_resource("liaopan1") - liaopan4 = self.deck.get_resource("liaopan4") - jipian1 = liaopan1.children[coin_num_N].children[0] - jipian4 = liaopan4.children[coin_num_N].children[0] - #print(jipian1) - #从料盘上去物料解绑后放到另一盘上 - jipian1.parent.unassign_child_resource(jipian1) - jipian4.parent.unassign_child_resource(jipian4) - #print(jipian2.parent) - battery = Battery(name = f"battery_{coin_num_N}") - battery.assign_child_resource(jipian1, location=None) - battery.assign_child_resource(jipian4, location=None) + # 收集当前电池的数据 + # 处理电池二维码 + try: + battery_qr_code = self.data_coin_cell_code + except Exception as e: + print(f"读取电池二维码失败: {e}") + battery_qr_code = "N/A" - zidanjia6 = self.deck.get_resource("zi_dan_jia6") - - zidanjia6.children[0].assign_child_resource(battery, location=None) - + # 处理电解液二维码 + try: + electrolyte_qr_code = self.data_electrolyte_code + except Exception as e: + print(f"读取电解液二维码失败: {e}") + electrolyte_qr_code = "N/A" + + # 处理开路电压 - 确保是数值类型 + try: + open_circuit_voltage = self.data_open_circuit_voltage + 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 = self.data_pole_weight + 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 + + battery_info = { + "battery_index": coin_num_N + 1, + "battery_barcode": battery_qr_code, + "electrolyte_barcode": electrolyte_qr_code, + "open_circuit_voltage": open_circuit_voltage, + "pole_weight": pole_weight, + "assembly_time": self.data_assembly_time, + "assembly_pressure": self.data_assembly_pressure, + "electrolyte_volume": self.data_electrolyte_volume + } + battery_data_list.append(battery_info) + print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['battery_barcode']}, 电解液码={battery_info['electrolyte_barcode']}") + + time.sleep(1) + # TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑 # 生成断点文件 # 生成包含elec_num_N、coin_num_N、timestamp的CSV文件 @@ -842,6 +1609,7 @@ def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp]) csvfile.flush() coin_num_N += 1 + self.coin_num_N = coin_num_N elec_use_num_N += 1 elec_num_N += 1 elec_use_num_N = 0 @@ -850,7 +1618,283 @@ def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell os.remove(summary_csv_file) #全部完成后等待依华发送完成信号 self.func_pack_send_finished_cmd() + + # 返回JSON格式数据 + result = { + "success": True, + "total_batteries": len(battery_data_list), + "batteries": battery_data_list, + "summary": { + "electrolyte_bottles_used": elec_num, + "batteries_per_bottle": elec_use_num, + "electrolyte_volume": elec_vol, + "assembly_type": assembly_type, + "assembly_pressure": assembly_pressure + } + } + + print(f"\n{'='*60}") + print(f"组装完成统计:") + print(f" 总组装电池数: {result['total_batteries']}") + print(f" 使用电解液瓶数: {elec_num}") + print(f" 每瓶电池数: {elec_use_num}") + print(f"{'='*60}\n") + + return result + + + def func_allpack_cmd_simp( + 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]: + """ + 简化版电池组装函数,整合了原 qiming_coin_cell_code 的参数设置和双滴模式 + + 此函数是 func_allpack_cmd 的增强版本,自动处理以下配置: + - 负极片和隔膜的盘数及矩阵点位 + - 枪头盒矩阵点位 + - 铝箔垫片使用设置 + - 压力模式和清洁忽略选项 + - 电解液双滴模式(分两次滴液) + + 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: 包含组装结果的字典 + """ + # 参数类型转换 + elec_num = int(elec_num) + elec_use_num = int(elec_use_num) + elec_vol = int(elec_vol) + dual_drop_first_volume = int(dual_drop_first_volume) + assembly_type = int(assembly_type) + assembly_pressure = int(assembly_pressure) + fujipian_panshu = int(fujipian_panshu) + fujipian_juzhendianwei = int(fujipian_juzhendianwei) + gemopanshu = int(gemopanshu) + gemo_juzhendianwei = int(gemo_juzhendianwei) + qiangtou_juzhendianwei = int(qiangtou_juzhendianwei) + + # 步骤1: 设置设备参数(原 qiming_coin_cell_code 的功能) + logger.info("=" * 60) + logger.info("设置设备参数...") + logger.info(f" 负极片盘数: {fujipian_panshu}, 矩阵点位: {fujipian_juzhendianwei}") + logger.info(f" 隔膜盘数: {gemopanshu}, 矩阵点位: {gemo_juzhendianwei}") + logger.info(f" 枪头盒矩阵点位: {qiangtou_juzhendianwei}") + logger.info(f" 铝箔垫片: {lvbodian}, 压力模式: {battery_pressure_mode}") + logger.info(f" 压制力: {assembly_pressure}") + logger.info(f" 忽略电池清洁: {battery_clean_ignore}") + logger.info("=" * 60) + + # 写入基础参数到PLC + self.client.use_node('REG_MSG_NE_PLATE_NUM').write(fujipian_panshu) + self.client.use_node('REG_MSG_NE_PLATE_MATRIX').write(fujipian_juzhendianwei) + self.client.use_node('REG_MSG_SEPARATOR_PLATE_NUM').write(gemopanshu) + self.client.use_node('REG_MSG_SEPARATOR_PLATE_MATRIX').write(gemo_juzhendianwei) + self.client.use_node('REG_MSG_TIP_BOX_MATRIX').write(qiangtou_juzhendianwei) + self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian) + self.client.use_node('REG_MSG_PRESS_MODE').write(not battery_pressure_mode) + self.client.use_node('REG_MSG_BATTERY_CLEAN_IGNORE').write(battery_clean_ignore) + + # 设置电解液双滴模式参数 + self.client.use_node('COIL_ELECTROLYTE_DUAL_DROP_MODE').write(dual_drop_mode) + self.client.use_node('REG_MSG_DUAL_DROP_FIRST_VOLUME').write(dual_drop_first_volume) + self.client.use_node('COIL_DUAL_DROP_SUCTION_TIMING').write(dual_drop_suction_timing) + self.client.use_node('COIL_DUAL_DROP_START_TIMING').write(dual_drop_start_timing) + + if dual_drop_mode: + logger.info(f"✓ 双滴模式已启用: 第一次排液={dual_drop_first_volume}μL, " + f"吸液时机={'先吸液' if dual_drop_suction_timing else '正常吸液'}, " + f"滴液时机={'正极片后' if dual_drop_start_timing else '正极片前'}") + else: + logger.info("✓ 单次滴液模式") + + logger.info("✓ 设备参数设置完成") + + # 步骤2: 执行组装流程(复用 func_allpack_cmd 的主体逻辑) + summary_csv_file = os.path.join(file_path, "duandian.csv") + + # 用于收集所有电池的数据 + battery_data_list = [] + + # 如果断点文件存在,先读取之前的进度 + if os.path.exists(summary_csv_file): + read_status_flag = True + with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.reader(csvfile) + header = next(reader) # 跳过标题行 + data_row = next(reader) # 读取数据行 + if len(data_row) >= 2: + elec_num_r = int(data_row[0]) + elec_use_num_r = int(data_row[1]) + elec_num_N = int(data_row[2]) + elec_use_num_N = int(data_row[3]) + coin_num_N = int(data_row[4]) + if elec_num_r == elec_num and elec_use_num_r == elec_use_num: + print("断点文件与当前任务匹配,继续") + else: + print("断点文件中elec_num、elec_use_num与当前任务不匹配,请检查任务下发参数或修改断点文件") + return { + "success": False, + "error": "断点文件参数不匹配", + "total_batteries": 0, + "batteries": [] + } + print(f"从断点文件读取进度: elec_num_N={elec_num_N}, elec_use_num_N={elec_use_num_N}, coin_num_N={coin_num_N}") + + else: + read_status_flag = False + print("未找到断点文件,从头开始") + elec_num_N = 0 + elec_use_num_N = 0 + coin_num_N = 0 + + for i in range(20): + print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}") + print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}") + print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}") + + last_i = elec_num_N + last_j = elec_use_num_N + for i in range(last_i, elec_num): + print(f"开始第{last_i+i+1}瓶电解液的组装") + # 第一个循环从上次断点继续,后续循环从0开始 + j_start = last_j if i == last_i else 0 + self.func_pack_send_msg_cmd(elec_use_num-j_start, elec_vol, assembly_type, assembly_pressure) + for j in range(j_start, elec_use_num): + print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装") + + # 读取电池组装数据并存入csv + self.func_pack_get_msg_cmd(file_path) + + # 收集当前电池的数据 + try: + battery_qr_code = self.data_coin_cell_code + except Exception as e: + print(f"读取电池二维码失败: {e}") + battery_qr_code = "N/A" + + try: + electrolyte_qr_code = self.data_electrolyte_code + except Exception as e: + print(f"读取电解液二维码失败: {e}") + electrolyte_qr_code = "N/A" + + try: + open_circuit_voltage = self.data_open_circuit_voltage + 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 = self.data_pole_weight + 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 + + battery_info = { + "battery_index": coin_num_N + 1, + "battery_barcode": battery_qr_code, + "electrolyte_barcode": electrolyte_qr_code, + "open_circuit_voltage": open_circuit_voltage, + "pole_weight": pole_weight, + "assembly_time": self.data_assembly_time, + "assembly_pressure": self.data_assembly_pressure, + "electrolyte_volume": self.data_electrolyte_volume + } + battery_data_list.append(battery_info) + print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['battery_barcode']}, 电解液码={battery_info['electrolyte_barcode']}") + + time.sleep(1) + + # 生成断点文件 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + with open(summary_csv_file, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(['elec_num','elec_use_num', 'elec_num_N', 'elec_use_num_N', 'coin_num_N', 'timestamp']) + writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp]) + csvfile.flush() + coin_num_N += 1 + self.coin_num_N = coin_num_N + elec_use_num_N += 1 + elec_num_N += 1 + elec_use_num_N = 0 + + # 循环正常结束,则删除断点文件 + os.remove(summary_csv_file) + # 全部完成后等待依华发送完成信号 + self.func_pack_send_finished_cmd() + + # 返回JSON格式数据 + result = { + "success": True, + "total_batteries": len(battery_data_list), + "batteries": battery_data_list, + "summary": { + "electrolyte_bottles_used": elec_num, + "batteries_per_bottle": elec_use_num, + "electrolyte_volume": elec_vol, + "assembly_type": assembly_type, + "assembly_pressure": assembly_pressure, + "dual_drop_mode": dual_drop_mode + } + } + + print(f"\n{'='*60}") + print(f"组装完成统计:") + print(f" 总组装电池数: {result['total_batteries']}") + print(f" 使用电解液瓶数: {elec_num}") + print(f" 每瓶电池数: {elec_use_num}") + print(f" 双滴模式: {'启用' if dual_drop_mode else '禁用'}") + print(f"{'='*60}\n") + + return result def func_pack_device_stop(self) -> bool: """打包指令:设备停止""" @@ -878,36 +1922,27 @@ def func_pack_device_stop(self) -> bool: def fun_wuliao_test(self) -> bool: #找到data_init中构建的2个物料盘 - #liaopan1 = self.deck.get_resource("liaopan1") - #liaopan4 = self.deck.get_resource("liaopan4") - #for coin_num_N in range(16): - # liaopan1 = self.deck.get_resource("liaopan1") - # liaopan4 = self.deck.get_resource("liaopan4") - # jipian1 = liaopan1.children[coin_num_N].children[0] - # jipian4 = liaopan4.children[coin_num_N].children[0] - # #print(jipian1) - # #从料盘上去物料解绑后放到另一盘上 - # jipian1.parent.unassign_child_resource(jipian1) - # jipian4.parent.unassign_child_resource(jipian4) - # - # #print(jipian2.parent) - # battery = Battery(name = f"battery_{coin_num_N}") - # battery.assign_child_resource(jipian1, location=None) - # battery.assign_child_resource(jipian4, location=None) - # - # zidanjia6 = self.deck.get_resource("zi_dan_jia6") - # zidanjia6.children[0].assign_child_resource(battery, location=None) - # ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ - # "resources": [self.deck] - # }) - # time.sleep(2) - for i in range(20): - print(f"输出{i}") - time.sleep(2) - + liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8") + for i in range(16): + battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2) + battery._unilabos_state = { + "diameter": 20.0, + "height": 20.0, + "assembly_pressure": i, + "electrolyte_volume": 20.0, + "electrolyte_name": f"DP{i}" + } + liaopan3.children[i].assign_child_resource(battery, location=None) + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [self.deck] + }) + # for i in range(40): + # print(f"fun_wuliao_test 运行结束{i}") + # time.sleep(1) + # time.sleep(40) # 数据读取与输出 - def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"): + def func_read_data_and_output(self, file_path: str="/Users/sml/work"): # 检查CSV导出是否正在运行,已运行则跳出,防止同时启动两个while循环 if self.csv_export_running: return False, "读取已在运行中" @@ -1012,7 +2047,7 @@ def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"): # else: # print("子弹夹洞位0没有极片") # - # #把电解液从瓶中取到电池夹子中 + # # TODO:#把电解液从瓶中取到电池夹子中 # battery_site = deck.get_resource("battery_press_1") # clip_magazine_battery = deck.get_resource("clip_magazine_battery") # if battery_site.has_battery(): @@ -1102,41 +2137,16 @@ def data_tips_inventory(self) -> int: ''' -if __name__ == "__main__": - from pylabrobot.resources import Resource - Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True) - #Coin_Cell.func_pack_device_init() - #Coin_Cell.func_pack_device_auto() - #Coin_Cell.func_pack_device_start() - #Coin_Cell.func_pack_send_bottle_num(2) - #Coin_Cell.func_pack_send_msg_cmd(2) - #Coin_Cell.func_pack_get_msg_cmd() - #Coin_Cell.func_pack_get_msg_cmd() - #Coin_Cell.func_pack_send_finished_cmd() -# - #Coin_Cell.func_allpack_cmd(3, 2) - #print(Coin_Cell.data_stack_vision_code) - #print("success") - #创建一个物料台面 - - #deck = create_a_coin_cell_deck() - - ##在台面上找到料盘和极片 - #liaopan1 = deck.get_resource("liaopan1") - #liaopan2 = deck.get_resource("liaopan2") - #jipian1 = liaopan1.children[1].children[0] -# - ##print(jipian1) - ##把物料解绑后放到另一盘上 - #jipian1.parent.unassign_child_resource(jipian1) - #liaopan2.children[1].assign_child_resource(jipian1, location=None) - ##print(jipian2.parent) - from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type - - with open("./button_battery_decks_unilab.json", "r", encoding="utf-8") as f: - bioyond_resources_unilab = json.load(f) - print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") - ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource]) - print(f"转换结果类型: {type(ulab_resources)}") - print(ulab_resources) +if __name__ == "__main__": + # 简单测试 + workstation = CoinCellAssemblyWorkstation(deck=CoincellDeck(setup=True, name="coin_cell_deck")) + # workstation.qiming_coin_cell_code(fujipian_panshu=1, fujipian_juzhendianwei=2, gemopanshu=3, gemo_juzhendianwei=4, lvbodian=False, battery_pressure_mode=False, battery_pressure=4200, battery_clean_ignore=False) + # print(f"工作站创建成功: {workstation.deck.name}") + # print(f"料盘数量: {len(workstation.deck.children)}") + workstation.func_pack_device_init() + workstation.func_pack_device_auto() + workstation.func_pack_device_start() + workstation.func_pack_send_bottle_num(16) + workstation.func_allpack_cmd(elec_num=16, elec_use_num=16, elec_vol=50, assembly_type=7, assembly_pressure=4200, file_path="/Users/calvincao/Desktop/work/Uni-Lab-OS-hhm") + \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv new file mode 100644 index 00000000..e46d1de5 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv @@ -0,0 +1,133 @@ +Name,DataType,InitValue,Comment,Attribute,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_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700, +COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd +COIL_SYS_START_STATUS,BOOL,,,,coil,8210, +COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220, +COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230, +COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240, +COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250, +COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260, +COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500, +COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status +REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000, +REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num +REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol +REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type +REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure +REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num +REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage +REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004, +REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006, +REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008, +REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight +REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time +REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure +REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume +REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num +REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code() +REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code() +REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code() +REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure +REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content +REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content +UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720, +UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520, +REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496, +REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000, +UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730, +UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530, +REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8 +REG_UNILAB_INTERACT,BOOL,,,,coil,8450, +,,,,,coil,8320, +COIL_ALUMINUM_FOIL,BOOL,,,,coil,8340, +REG_MSG_NE_PLATE_MATRIX,INT16,,,,hold_register,440, +REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,,,hold_register,450, +REG_MSG_TIP_BOX_MATRIX,INT16,,,,hold_register,480, +REG_MSG_NE_PLATE_NUM,INT16,,,,hold_register,443, +REG_MSG_SEPARATOR_PLATE_NUM,INT16,,,,hold_register,453, +REG_MSG_PRESS_MODE,BOOL,,,,coil,8360, +,BOOL,,,,coil,8300, +,BOOL,,,,coil,8310, +COIL_GB_L_IGNORE_CMD,BOOL,,,,coil,8320, +COIL_GB_R_IGNORE_CMD,BOOL,,,,coil,8420, +,BOOL,,,,coil,8350, +COIL_ELECTROLYTE_DUAL_DROP_MODE,BOOL,,,,coil,8370, +,BOOL,,,,coil,8380, +,BOOL,,,,coil,8390, +,BOOL,,,,coil,8400, +,BOOL,,,,coil,8410, +REG_MSG_DUAL_DROP_FIRST_VOLUME,INT16,,,,hold_register,4001, +COIL_DUAL_DROP_SUCTION_TIMING,BOOL,,,,coil,8430, +COIL_DUAL_DROP_START_TIMING,BOOL,,,,coil,8470, +REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,,,coil,8460, +COIL_MATERIAL_SEARCH_DIALOG_APPEAR,BOOL,,,,coil,6470, +COIL_MATERIAL_SEARCH_CONFIRM_YES,BOOL,,,,coil,6480, +COIL_MATERIAL_SEARCH_CONFIRM_NO,BOOL,,,,coil,6490, +COIL_ALARM_100_SYSTEM_ERROR,BOOL,,,,coil,1000,异常100-系统异常 +COIL_ALARM_101_EMERGENCY_STOP,BOOL,,,,coil,1010,异常101-急停 +COIL_ALARM_111_GLOVEBOX_EMERGENCY_STOP,BOOL,,,,coil,1110,异常111-手套箱急停 +COIL_ALARM_112_GLOVEBOX_GRATING_BLOCKED,BOOL,,,,coil,1120,异常112-手套箱内光栅遮挡 +COIL_ALARM_160_PIPETTE_TIP_SHORTAGE,BOOL,,,,coil,1600,异常160-移液枪头缺料 +COIL_ALARM_161_POSITIVE_SHELL_SHORTAGE,BOOL,,,,coil,1610,异常161-正极壳缺料 +COIL_ALARM_162_ALUMINUM_FOIL_SHORTAGE,BOOL,,,,coil,1620,异常162-铝箔垫缺料 +COIL_ALARM_163_POSITIVE_PLATE_SHORTAGE,BOOL,,,,coil,1630,异常163-正极片缺料 +COIL_ALARM_164_SEPARATOR_SHORTAGE,BOOL,,,,coil,1640,异常164-隔膜缺料 +COIL_ALARM_165_NEGATIVE_PLATE_SHORTAGE,BOOL,,,,coil,1650,异常165-负极片缺料 +COIL_ALARM_166_FLAT_WASHER_SHORTAGE,BOOL,,,,coil,1660,异常166-平垫缺料 +COIL_ALARM_167_SPRING_WASHER_SHORTAGE,BOOL,,,,coil,1670,异常167-弹垫缺料 +COIL_ALARM_168_NEGATIVE_SHELL_SHORTAGE,BOOL,,,,coil,1680,异常168-负极壳缺料 +COIL_ALARM_169_FINISHED_BATTERY_FULL,BOOL,,,,coil,1690,异常169-成品电池满料 +COIL_ALARM_201_SERVO_AXIS_01_ERROR,BOOL,,,,coil,2010,异常201-伺服轴01异常 +COIL_ALARM_202_SERVO_AXIS_02_ERROR,BOOL,,,,coil,2020,异常202-伺服轴02异常 +COIL_ALARM_203_SERVO_AXIS_03_ERROR,BOOL,,,,coil,2030,异常203-伺服轴03异常 +COIL_ALARM_204_SERVO_AXIS_04_ERROR,BOOL,,,,coil,2040,异常204-伺服轴04异常 +COIL_ALARM_205_SERVO_AXIS_05_ERROR,BOOL,,,,coil,2050,异常205-伺服轴05异常 +COIL_ALARM_206_SERVO_AXIS_06_ERROR,BOOL,,,,coil,2060,异常206-伺服轴06异常 +COIL_ALARM_207_SERVO_AXIS_07_ERROR,BOOL,,,,coil,2070,异常207-伺服轴07异常 +COIL_ALARM_208_SERVO_AXIS_08_ERROR,BOOL,,,,coil,2080,异常208-伺服轴08异常 +COIL_ALARM_209_SERVO_AXIS_09_ERROR,BOOL,,,,coil,2090,异常209-伺服轴09异常 +COIL_ALARM_210_SERVO_AXIS_10_ERROR,BOOL,,,,coil,2100,异常210-伺服轴10异常 +COIL_ALARM_211_SERVO_AXIS_11_ERROR,BOOL,,,,coil,2110,异常211-伺服轴11异常 +COIL_ALARM_212_SERVO_AXIS_12_ERROR,BOOL,,,,coil,2120,异常212-伺服轴12异常 +COIL_ALARM_213_SERVO_AXIS_13_ERROR,BOOL,,,,coil,2130,异常213-伺服轴13异常 +COIL_ALARM_214_SERVO_AXIS_14_ERROR,BOOL,,,,coil,2140,异常214-伺服轴14异常 +COIL_ALARM_250_OTHER_COMPONENT_ERROR,BOOL,,,,coil,2500,异常250-其他元件异常 +COIL_ALARM_251_PIPETTE_COMM_ERROR,BOOL,,,,coil,2510,异常251-移液枪通讯异常 +COIL_ALARM_252_PIPETTE_ALARM,BOOL,,,,coil,2520,异常252-移液枪报警 +COIL_ALARM_256_ELECTRIC_GRIPPER_ERROR,BOOL,,,,coil,2560,异常256-电爪异常 +COIL_ALARM_262_RB_UNKNOWN_POSITION_ERROR,BOOL,,,,coil,2620,异常262-RB报警:未知点位错误 +COIL_ALARM_263_RB_XYZ_PARAM_LIMIT_ERROR,BOOL,,,,coil,2630,异常263-RB报警:X、Y、Z参数超限制 +COIL_ALARM_264_RB_VISION_PARAM_ERROR,BOOL,,,,coil,2640,异常264-RB报警:视觉参数误差过大 +COIL_ALARM_265_RB_NOZZLE_1_PICK_FAIL,BOOL,,,,coil,2650,异常265-RB报警:1#吸嘴取料失败 +COIL_ALARM_266_RB_NOZZLE_2_PICK_FAIL,BOOL,,,,coil,2660,异常266-RB报警:2#吸嘴取料失败 +COIL_ALARM_267_RB_NOZZLE_3_PICK_FAIL,BOOL,,,,coil,2670,异常267-RB报警:3#吸嘴取料失败 +COIL_ALARM_268_RB_NOZZLE_4_PICK_FAIL,BOOL,,,,coil,2680,异常268-RB报警:4#吸嘴取料失败 +COIL_ALARM_269_RB_TRAY_PICK_FAIL,BOOL,,,,coil,2690,异常269-RB报警:取物料盘失败 +COIL_ALARM_280_RB_COLLISION_ERROR,BOOL,,,,coil,2800,异常280-RB碰撞异常 +COIL_ALARM_290_VISION_SYSTEM_COMM_ERROR,BOOL,,,,coil,2900,异常290-视觉系统通讯异常 +COIL_ALARM_291_VISION_ALIGNMENT_NG,BOOL,,,,coil,2910,异常291-视觉对位NG异常 +COIL_ALARM_292_BARCODE_SCANNER_COMM_ERROR,BOOL,,,,coil,2920,异常292-扫码枪通讯异常 +COIL_ALARM_310_OCV_TRANSFER_NOZZLE_SUCTION_ERROR,BOOL,,,,coil,3100,异常310-开电移载吸嘴吸真空异常 +COIL_ALARM_311_OCV_TRANSFER_NOZZLE_BREAK_ERROR,BOOL,,,,coil,3110,异常311-开电移载吸嘴破真空异常 +COIL_ALARM_312_WEIGHT_TRANSFER_NOZZLE_SUCTION_ERROR,BOOL,,,,coil,3120,异常312-称重移载吸嘴吸真空异常 +COIL_ALARM_313_WEIGHT_TRANSFER_NOZZLE_BREAK_ERROR,BOOL,,,,coil,3130,异常313-称重移载吸嘴破真空异常 +COIL_ALARM_340_OCV_NOZZLE_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3400,异常340-开路电压吸嘴移载气缸异常 +COIL_ALARM_342_OCV_NOZZLE_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3420,异常342-开路电压吸嘴升降气缸异常 +COIL_ALARM_344_OCV_CRIMPING_CYLINDER_ERROR,BOOL,,,,coil,3440,异常344-开路电压旋压气缸异常 +COIL_ALARM_350_WEIGHT_NOZZLE_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3500,异常350-称重吸嘴移载气缸异常 +COIL_ALARM_352_WEIGHT_NOZZLE_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3520,异常352-称重吸嘴升降气缸异常 +COIL_ALARM_354_CLEANING_CLOTH_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3540,异常354-清洗无尘布移载气缸异常 +COIL_ALARM_356_CLEANING_CLOTH_PRESS_CYLINDER_ERROR,BOOL,,,,coil,3560,异常356-清洗无尘布压紧气缸异常 +COIL_ALARM_360_ELECTROLYTE_BOTTLE_POSITION_CYLINDER_ERROR,BOOL,,,,coil,3600,异常360-电解液瓶定位气缸异常 +COIL_ALARM_362_PIPETTE_TIP_BOX_POSITION_CYLINDER_ERROR,BOOL,,,,coil,3620,异常362-移液枪头盒定位气缸异常 +COIL_ALARM_364_REAGENT_BOTTLE_GRIPPER_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3640,异常364-试剂瓶夹爪升降气缸异常 +COIL_ALARM_366_REAGENT_BOTTLE_GRIPPER_CYLINDER_ERROR,BOOL,,,,coil,3660,异常366-试剂瓶夹爪气缸异常 +COIL_ALARM_370_PRESS_MODULE_BLOW_CYLINDER_ERROR,BOOL,,,,coil,3700,异常370-压制模块吹气气缸异常 +COIL_ALARM_151_ELECTROLYTE_BOTTLE_POSITION_ERROR,BOOL,,,,coil,1510,异常151-电解液瓶定位在籍异常 +COIL_ALARM_152_ELECTROLYTE_BOTTLE_CAP_ERROR,BOOL,,,,coil,1520,异常152-电解液瓶盖在籍异常 diff --git a/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json deleted file mode 100644 index 7e371327..00000000 --- a/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json +++ /dev/null @@ -1,14472 +0,0 @@ -{ - "nodes": [ - { - "id": "BatteryStation", - "name": "扣电工作站", - "children": [ - "coin_cell_deck" - ], - "parent": null, - "type": "device", - "class": "bettery_station_registry", - "position": { - "x": 600, - "y": 400, - "z": 0 - }, - "config": { - "debug_mode": false, - "_comment": "protocol_type接外部工站固定写法字段,一般为空,deck写法也固定", - "protocol_type": [], - "deck": { - "data": { - "_resource_child_name": "coin_cell_deck", - "_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck" - } - }, - - "address": "192.168.1.20", - "port": 502 - }, - "data": {} - }, - { - "id": "coin_cell_deck", - "name": "coin_cell_deck", - "sample_id": null, - "children": [ - "zi_dan_jia", - "zi_dan_jia2", - "zi_dan_jia3", - "zi_dan_jia4", - "zi_dan_jia5", - "zi_dan_jia6", - "zi_dan_jia7", - "zi_dan_jia8", - "liaopan1", - "liaopan2", - "liaopan3", - "liaopan4", - "liaopan5", - "liaopan6", - "bottle_rack_3x4", - "bottle_rack_6x2", - "bottle_rack_6x2_2", - "tip_box_64", - "waste_tip_box" - ], - "parent": null, - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "CoincellDeck", - "size_x": 1620.0, - "size_y": 1270.0, - "size_z": 500.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "coin_cell_deck", - "barcode": null - }, - "data": {} - }, - { - "id": "zi_dan_jia", - "name": "zi_dan_jia", - "sample_id": null, - "children": [ - "zi_dan_jia_clipmagazinehole_0_0", - "zi_dan_jia_clipmagazinehole_0_1", - "zi_dan_jia_clipmagazinehole_1_0", - "zi_dan_jia_clipmagazinehole_1_1" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1400, - "y": 50, - "z": 0 - }, - "config": { - "type": "ClipMagazine_four", - "size_x": 80, - "size_y": 80, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_four", - "model": null, - "barcode": null, - "ordering": { - "A1": "zi_dan_jia_clipmagazinehole_0_0", - "B1": "zi_dan_jia_clipmagazinehole_0_1", - "A2": "zi_dan_jia_clipmagazinehole_1_0", - "B2": "zi_dan_jia_clipmagazinehole_1_1" - }, - "hole_diameter": 14.0, - "hole_depth": 10.0, - "max_sheets_per_hole": 100 - }, - "data": {} - }, - { - "id": "zi_dan_jia_clipmagazinehole_0_0", - "name": "zi_dan_jia_clipmagazinehole_0_0", - "sample_id": null, - "children": [ - "zi_dan_jia2_jipian_0" - ], - "parent": "zi_dan_jia", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia2_jipian_0", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia_clipmagazinehole_0_0" - } - ] - } - }, - { - "id": "zi_dan_jia2_jipian_0", - "name": "zi_dan_jia2_jipian_0", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia_clipmagazinehole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia_clipmagazinehole_0_1", - "name": "zi_dan_jia_clipmagazinehole_0_1", - "sample_id": null, - "children": [ - "zi_dan_jia2_jipian_1" - ], - "parent": "zi_dan_jia", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia2_jipian_1", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia_clipmagazinehole_0_1" - } - ] - } - }, - { - "id": "zi_dan_jia2_jipian_1", - "name": "zi_dan_jia2_jipian_1", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia_clipmagazinehole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia_clipmagazinehole_1_0", - "name": "zi_dan_jia_clipmagazinehole_1_0", - "sample_id": null, - "children": [ - "zi_dan_jia2_jipian_2" - ], - "parent": "zi_dan_jia", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia2_jipian_2", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia_clipmagazinehole_1_0" - } - ] - } - }, - { - "id": "zi_dan_jia2_jipian_2", - "name": "zi_dan_jia2_jipian_2", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia_clipmagazinehole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia_clipmagazinehole_1_1", - "name": "zi_dan_jia_clipmagazinehole_1_1", - "sample_id": null, - "children": [ - "zi_dan_jia2_jipian_3" - ], - "parent": "zi_dan_jia", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia2_jipian_3", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia_clipmagazinehole_1_1" - } - ] - } - }, - { - "id": "zi_dan_jia2_jipian_3", - "name": "zi_dan_jia2_jipian_3", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia_clipmagazinehole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia2", - "name": "zi_dan_jia2", - "sample_id": null, - "children": [ - "zi_dan_jia2_clipmagazinehole_0_0", - "zi_dan_jia2_clipmagazinehole_0_1", - "zi_dan_jia2_clipmagazinehole_1_0", - "zi_dan_jia2_clipmagazinehole_1_1" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1600, - "y": 200, - "z": 0 - }, - "config": { - "type": "ClipMagazine_four", - "size_x": 80, - "size_y": 80, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_four", - "model": null, - "barcode": null, - "ordering": { - "A1": "zi_dan_jia2_clipmagazinehole_0_0", - "B1": "zi_dan_jia2_clipmagazinehole_0_1", - "A2": "zi_dan_jia2_clipmagazinehole_1_0", - "B2": "zi_dan_jia2_clipmagazinehole_1_1" - }, - "hole_diameter": 14.0, - "hole_depth": 10.0, - "max_sheets_per_hole": 100 - }, - "data": {} - }, - { - "id": "zi_dan_jia2_clipmagazinehole_0_0", - "name": "zi_dan_jia2_clipmagazinehole_0_0", - "sample_id": null, - "children": [ - "zi_dan_jia_jipian_0" - ], - "parent": "zi_dan_jia2", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia_jipian_0", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia2_clipmagazinehole_0_0" - } - ] - } - }, - { - "id": "zi_dan_jia_jipian_0", - "name": "zi_dan_jia_jipian_0", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia2_clipmagazinehole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia2_clipmagazinehole_0_1", - "name": "zi_dan_jia2_clipmagazinehole_0_1", - "sample_id": null, - "children": [ - "zi_dan_jia_jipian_1" - ], - "parent": "zi_dan_jia2", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia_jipian_1", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia2_clipmagazinehole_0_1" - } - ] - } - }, - { - "id": "zi_dan_jia_jipian_1", - "name": "zi_dan_jia_jipian_1", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia2_clipmagazinehole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia2_clipmagazinehole_1_0", - "name": "zi_dan_jia2_clipmagazinehole_1_0", - "sample_id": null, - "children": [ - "zi_dan_jia_jipian_2" - ], - "parent": "zi_dan_jia2", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia_jipian_2", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia2_clipmagazinehole_1_0" - } - ] - } - }, - { - "id": "zi_dan_jia_jipian_2", - "name": "zi_dan_jia_jipian_2", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia2_clipmagazinehole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia2_clipmagazinehole_1_1", - "name": "zi_dan_jia2_clipmagazinehole_1_1", - "sample_id": null, - "children": [ - "zi_dan_jia_jipian_3" - ], - "parent": "zi_dan_jia2", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia_jipian_3", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia2_clipmagazinehole_1_1" - } - ] - } - }, - { - "id": "zi_dan_jia_jipian_3", - "name": "zi_dan_jia_jipian_3", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia2_clipmagazinehole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia3", - "name": "zi_dan_jia3", - "sample_id": null, - "children": [ - "zi_dan_jia3_clipmagazinehole_0_0", - "zi_dan_jia3_clipmagazinehole_0_1", - "zi_dan_jia3_clipmagazinehole_1_0", - "zi_dan_jia3_clipmagazinehole_1_1", - "zi_dan_jia3_clipmagazinehole_2_0", - "zi_dan_jia3_clipmagazinehole_2_1" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1500, - "y": 200, - "z": 0 - }, - "config": { - "type": "ClipMagazine", - "size_x": 80, - "size_y": 80, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine", - "model": null, - "barcode": null, - "ordering": { - "A1": "zi_dan_jia3_clipmagazinehole_0_0", - "B1": "zi_dan_jia3_clipmagazinehole_0_1", - "A2": "zi_dan_jia3_clipmagazinehole_1_0", - "B2": "zi_dan_jia3_clipmagazinehole_1_1", - "A3": "zi_dan_jia3_clipmagazinehole_2_0", - "B3": "zi_dan_jia3_clipmagazinehole_2_1" - }, - "hole_diameter": 14.0, - "hole_depth": 10.0, - "max_sheets_per_hole": 100 - }, - "data": {} - }, - { - "id": "zi_dan_jia3_clipmagazinehole_0_0", - "name": "zi_dan_jia3_clipmagazinehole_0_0", - "sample_id": null, - "children": [ - "zi_dan_jia3_jipian_0" - ], - "parent": "zi_dan_jia3", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia3_jipian_0", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia3_clipmagazinehole_0_0" - } - ] - } - }, - { - "id": "zi_dan_jia3_jipian_0", - "name": "zi_dan_jia3_jipian_0", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia3_clipmagazinehole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia3_clipmagazinehole_0_1", - "name": "zi_dan_jia3_clipmagazinehole_0_1", - "sample_id": null, - "children": [ - "zi_dan_jia3_jipian_1" - ], - "parent": "zi_dan_jia3", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia3_jipian_1", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia3_clipmagazinehole_0_1" - } - ] - } - }, - { - "id": "zi_dan_jia3_jipian_1", - "name": "zi_dan_jia3_jipian_1", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia3_clipmagazinehole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia3_clipmagazinehole_1_0", - "name": "zi_dan_jia3_clipmagazinehole_1_0", - "sample_id": null, - "children": [ - "zi_dan_jia3_jipian_2" - ], - "parent": "zi_dan_jia3", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia3_jipian_2", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia3_clipmagazinehole_1_0" - } - ] - } - }, - { - "id": "zi_dan_jia3_jipian_2", - "name": "zi_dan_jia3_jipian_2", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia3_clipmagazinehole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia3_clipmagazinehole_1_1", - "name": "zi_dan_jia3_clipmagazinehole_1_1", - "sample_id": null, - "children": [ - "zi_dan_jia3_jipian_3" - ], - "parent": "zi_dan_jia3", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia3_jipian_3", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia3_clipmagazinehole_1_1" - } - ] - } - }, - { - "id": "zi_dan_jia3_jipian_3", - "name": "zi_dan_jia3_jipian_3", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia3_clipmagazinehole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia3_clipmagazinehole_2_0", - "name": "zi_dan_jia3_clipmagazinehole_2_0", - "sample_id": null, - "children": [ - "zi_dan_jia3_jipian_4" - ], - "parent": "zi_dan_jia3", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia3_jipian_4", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia3_clipmagazinehole_2_0" - } - ] - } - }, - { - "id": "zi_dan_jia3_jipian_4", - "name": "zi_dan_jia3_jipian_4", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia3_clipmagazinehole_2_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia3_clipmagazinehole_2_1", - "name": "zi_dan_jia3_clipmagazinehole_2_1", - "sample_id": null, - "children": [ - "zi_dan_jia3_jipian_5" - ], - "parent": "zi_dan_jia3", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia3_jipian_5", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia3_clipmagazinehole_2_1" - } - ] - } - }, - { - "id": "zi_dan_jia3_jipian_5", - "name": "zi_dan_jia3_jipian_5", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia3_clipmagazinehole_2_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia4", - "name": "zi_dan_jia4", - "sample_id": null, - "children": [ - "zi_dan_jia4_clipmagazinehole_0_0", - "zi_dan_jia4_clipmagazinehole_0_1", - "zi_dan_jia4_clipmagazinehole_1_0", - "zi_dan_jia4_clipmagazinehole_1_1", - "zi_dan_jia4_clipmagazinehole_2_0", - "zi_dan_jia4_clipmagazinehole_2_1" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1500, - "y": 300, - "z": 0 - }, - "config": { - "type": "ClipMagazine", - "size_x": 80, - "size_y": 80, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine", - "model": null, - "barcode": null, - "ordering": { - "A1": "zi_dan_jia4_clipmagazinehole_0_0", - "B1": "zi_dan_jia4_clipmagazinehole_0_1", - "A2": "zi_dan_jia4_clipmagazinehole_1_0", - "B2": "zi_dan_jia4_clipmagazinehole_1_1", - "A3": "zi_dan_jia4_clipmagazinehole_2_0", - "B3": "zi_dan_jia4_clipmagazinehole_2_1" - }, - "hole_diameter": 14.0, - "hole_depth": 10.0, - "max_sheets_per_hole": 100 - }, - "data": {} - }, - { - "id": "zi_dan_jia4_clipmagazinehole_0_0", - "name": "zi_dan_jia4_clipmagazinehole_0_0", - "sample_id": null, - "children": [ - "zi_dan_jia4_jipian_0" - ], - "parent": "zi_dan_jia4", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia4_jipian_0", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia4_clipmagazinehole_0_0" - } - ] - } - }, - { - "id": "zi_dan_jia4_jipian_0", - "name": "zi_dan_jia4_jipian_0", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia4_clipmagazinehole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia4_clipmagazinehole_0_1", - "name": "zi_dan_jia4_clipmagazinehole_0_1", - "sample_id": null, - "children": [ - "zi_dan_jia4_jipian_1" - ], - "parent": "zi_dan_jia4", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia4_jipian_1", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia4_clipmagazinehole_0_1" - } - ] - } - }, - { - "id": "zi_dan_jia4_jipian_1", - "name": "zi_dan_jia4_jipian_1", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia4_clipmagazinehole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia4_clipmagazinehole_1_0", - "name": "zi_dan_jia4_clipmagazinehole_1_0", - "sample_id": null, - "children": [ - "zi_dan_jia4_jipian_2" - ], - "parent": "zi_dan_jia4", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia4_jipian_2", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia4_clipmagazinehole_1_0" - } - ] - } - }, - { - "id": "zi_dan_jia4_jipian_2", - "name": "zi_dan_jia4_jipian_2", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia4_clipmagazinehole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia4_clipmagazinehole_1_1", - "name": "zi_dan_jia4_clipmagazinehole_1_1", - "sample_id": null, - "children": [ - "zi_dan_jia4_jipian_3" - ], - "parent": "zi_dan_jia4", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia4_jipian_3", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia4_clipmagazinehole_1_1" - } - ] - } - }, - { - "id": "zi_dan_jia4_jipian_3", - "name": "zi_dan_jia4_jipian_3", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia4_clipmagazinehole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia4_clipmagazinehole_2_0", - "name": "zi_dan_jia4_clipmagazinehole_2_0", - "sample_id": null, - "children": [ - "zi_dan_jia4_jipian_4" - ], - "parent": "zi_dan_jia4", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia4_jipian_4", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia4_clipmagazinehole_2_0" - } - ] - } - }, - { - "id": "zi_dan_jia4_jipian_4", - "name": "zi_dan_jia4_jipian_4", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia4_clipmagazinehole_2_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia4_clipmagazinehole_2_1", - "name": "zi_dan_jia4_clipmagazinehole_2_1", - "sample_id": null, - "children": [ - "zi_dan_jia4_jipian_5" - ], - "parent": "zi_dan_jia4", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia4_jipian_5", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia4_clipmagazinehole_2_1" - } - ] - } - }, - { - "id": "zi_dan_jia4_jipian_5", - "name": "zi_dan_jia4_jipian_5", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia4_clipmagazinehole_2_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia5", - "name": "zi_dan_jia5", - "sample_id": null, - "children": [ - "zi_dan_jia5_clipmagazinehole_0_0", - "zi_dan_jia5_clipmagazinehole_0_1", - "zi_dan_jia5_clipmagazinehole_1_0", - "zi_dan_jia5_clipmagazinehole_1_1", - "zi_dan_jia5_clipmagazinehole_2_0", - "zi_dan_jia5_clipmagazinehole_2_1" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1600, - "y": 300, - "z": 0 - }, - "config": { - "type": "ClipMagazine", - "size_x": 80, - "size_y": 80, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine", - "model": null, - "barcode": null, - "ordering": { - "A1": "zi_dan_jia5_clipmagazinehole_0_0", - "B1": "zi_dan_jia5_clipmagazinehole_0_1", - "A2": "zi_dan_jia5_clipmagazinehole_1_0", - "B2": "zi_dan_jia5_clipmagazinehole_1_1", - "A3": "zi_dan_jia5_clipmagazinehole_2_0", - "B3": "zi_dan_jia5_clipmagazinehole_2_1" - }, - "hole_diameter": 14.0, - "hole_depth": 10.0, - "max_sheets_per_hole": 100 - }, - "data": {} - }, - { - "id": "zi_dan_jia5_clipmagazinehole_0_0", - "name": "zi_dan_jia5_clipmagazinehole_0_0", - "sample_id": null, - "children": [ - "zi_dan_jia5_jipian_0" - ], - "parent": "zi_dan_jia5", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia5_jipian_0", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia5_clipmagazinehole_0_0" - } - ] - } - }, - { - "id": "zi_dan_jia5_jipian_0", - "name": "zi_dan_jia5_jipian_0", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia5_clipmagazinehole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia5_clipmagazinehole_0_1", - "name": "zi_dan_jia5_clipmagazinehole_0_1", - "sample_id": null, - "children": [ - "zi_dan_jia5_jipian_1" - ], - "parent": "zi_dan_jia5", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia5_jipian_1", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia5_clipmagazinehole_0_1" - } - ] - } - }, - { - "id": "zi_dan_jia5_jipian_1", - "name": "zi_dan_jia5_jipian_1", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia5_clipmagazinehole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia5_clipmagazinehole_1_0", - "name": "zi_dan_jia5_clipmagazinehole_1_0", - "sample_id": null, - "children": [ - "zi_dan_jia5_jipian_2" - ], - "parent": "zi_dan_jia5", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia5_jipian_2", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia5_clipmagazinehole_1_0" - } - ] - } - }, - { - "id": "zi_dan_jia5_jipian_2", - "name": "zi_dan_jia5_jipian_2", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia5_clipmagazinehole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia5_clipmagazinehole_1_1", - "name": "zi_dan_jia5_clipmagazinehole_1_1", - "sample_id": null, - "children": [ - "zi_dan_jia5_jipian_3" - ], - "parent": "zi_dan_jia5", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia5_jipian_3", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia5_clipmagazinehole_1_1" - } - ] - } - }, - { - "id": "zi_dan_jia5_jipian_3", - "name": "zi_dan_jia5_jipian_3", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia5_clipmagazinehole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia5_clipmagazinehole_2_0", - "name": "zi_dan_jia5_clipmagazinehole_2_0", - "sample_id": null, - "children": [ - "zi_dan_jia5_jipian_4" - ], - "parent": "zi_dan_jia5", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia5_jipian_4", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia5_clipmagazinehole_2_0" - } - ] - } - }, - { - "id": "zi_dan_jia5_jipian_4", - "name": "zi_dan_jia5_jipian_4", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia5_clipmagazinehole_2_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia5_clipmagazinehole_2_1", - "name": "zi_dan_jia5_clipmagazinehole_2_1", - "sample_id": null, - "children": [ - "zi_dan_jia5_jipian_5" - ], - "parent": "zi_dan_jia5", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia5_jipian_5", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia5_clipmagazinehole_2_1" - } - ] - } - }, - { - "id": "zi_dan_jia5_jipian_5", - "name": "zi_dan_jia5_jipian_5", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia5_clipmagazinehole_2_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia6", - "name": "zi_dan_jia6", - "sample_id": null, - "children": [ - "zi_dan_jia6_clipmagazinehole_0_0", - "zi_dan_jia6_clipmagazinehole_0_1", - "zi_dan_jia6_clipmagazinehole_1_0", - "zi_dan_jia6_clipmagazinehole_1_1", - "zi_dan_jia6_clipmagazinehole_2_0", - "zi_dan_jia6_clipmagazinehole_2_1" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1530, - "y": 500, - "z": 0 - }, - "config": { - "type": "ClipMagazine", - "size_x": 80, - "size_y": 80, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine", - "model": null, - "barcode": null, - "ordering": { - "A1": "zi_dan_jia6_clipmagazinehole_0_0", - "B1": "zi_dan_jia6_clipmagazinehole_0_1", - "A2": "zi_dan_jia6_clipmagazinehole_1_0", - "B2": "zi_dan_jia6_clipmagazinehole_1_1", - "A3": "zi_dan_jia6_clipmagazinehole_2_0", - "B3": "zi_dan_jia6_clipmagazinehole_2_1" - }, - "hole_diameter": 14.0, - "hole_depth": 10.0, - "max_sheets_per_hole": 100 - }, - "data": {} - }, - { - "id": "zi_dan_jia6_clipmagazinehole_0_0", - "name": "zi_dan_jia6_clipmagazinehole_0_0", - "sample_id": null, - "children": [ - "zi_dan_jia6_jipian_0" - ], - "parent": "zi_dan_jia6", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia6_jipian_0", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia6_clipmagazinehole_0_0" - } - ] - } - }, - { - "id": "zi_dan_jia6_jipian_0", - "name": "zi_dan_jia6_jipian_0", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia6_clipmagazinehole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia6_clipmagazinehole_0_1", - "name": "zi_dan_jia6_clipmagazinehole_0_1", - "sample_id": null, - "children": [ - "zi_dan_jia6_jipian_1" - ], - "parent": "zi_dan_jia6", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia6_jipian_1", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia6_clipmagazinehole_0_1" - } - ] - } - }, - { - "id": "zi_dan_jia6_jipian_1", - "name": "zi_dan_jia6_jipian_1", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia6_clipmagazinehole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia6_clipmagazinehole_1_0", - "name": "zi_dan_jia6_clipmagazinehole_1_0", - "sample_id": null, - "children": [ - "zi_dan_jia6_jipian_2" - ], - "parent": "zi_dan_jia6", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia6_jipian_2", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia6_clipmagazinehole_1_0" - } - ] - } - }, - { - "id": "zi_dan_jia6_jipian_2", - "name": "zi_dan_jia6_jipian_2", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia6_clipmagazinehole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia6_clipmagazinehole_1_1", - "name": "zi_dan_jia6_clipmagazinehole_1_1", - "sample_id": null, - "children": [ - "zi_dan_jia6_jipian_3" - ], - "parent": "zi_dan_jia6", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia6_jipian_3", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia6_clipmagazinehole_1_1" - } - ] - } - }, - { - "id": "zi_dan_jia6_jipian_3", - "name": "zi_dan_jia6_jipian_3", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia6_clipmagazinehole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia6_clipmagazinehole_2_0", - "name": "zi_dan_jia6_clipmagazinehole_2_0", - "sample_id": null, - "children": [ - "zi_dan_jia6_jipian_4" - ], - "parent": "zi_dan_jia6", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia6_jipian_4", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia6_clipmagazinehole_2_0" - } - ] - } - }, - { - "id": "zi_dan_jia6_jipian_4", - "name": "zi_dan_jia6_jipian_4", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia6_clipmagazinehole_2_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia6_clipmagazinehole_2_1", - "name": "zi_dan_jia6_clipmagazinehole_2_1", - "sample_id": null, - "children": [ - "zi_dan_jia6_jipian_5" - ], - "parent": "zi_dan_jia6", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia6_jipian_5", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia6_clipmagazinehole_2_1" - } - ] - } - }, - { - "id": "zi_dan_jia6_jipian_5", - "name": "zi_dan_jia6_jipian_5", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia6_clipmagazinehole_2_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia7", - "name": "zi_dan_jia7", - "sample_id": null, - "children": [ - "zi_dan_jia7_clipmagazinehole_0_0", - "zi_dan_jia7_clipmagazinehole_0_1", - "zi_dan_jia7_clipmagazinehole_1_0", - "zi_dan_jia7_clipmagazinehole_1_1", - "zi_dan_jia7_clipmagazinehole_2_0", - "zi_dan_jia7_clipmagazinehole_2_1" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1180, - "y": 400, - "z": 0 - }, - "config": { - "type": "ClipMagazine", - "size_x": 80, - "size_y": 80, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine", - "model": null, - "barcode": null, - "ordering": { - "A1": "zi_dan_jia7_clipmagazinehole_0_0", - "B1": "zi_dan_jia7_clipmagazinehole_0_1", - "A2": "zi_dan_jia7_clipmagazinehole_1_0", - "B2": "zi_dan_jia7_clipmagazinehole_1_1", - "A3": "zi_dan_jia7_clipmagazinehole_2_0", - "B3": "zi_dan_jia7_clipmagazinehole_2_1" - }, - "hole_diameter": 14.0, - "hole_depth": 10.0, - "max_sheets_per_hole": 100 - }, - "data": {} - }, - { - "id": "zi_dan_jia7_clipmagazinehole_0_0", - "name": "zi_dan_jia7_clipmagazinehole_0_0", - "sample_id": null, - "children": [ - "zi_dan_jia7_jipian_0" - ], - "parent": "zi_dan_jia7", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia7_jipian_0", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia7_clipmagazinehole_0_0" - } - ] - } - }, - { - "id": "zi_dan_jia7_jipian_0", - "name": "zi_dan_jia7_jipian_0", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia7_clipmagazinehole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia7_clipmagazinehole_0_1", - "name": "zi_dan_jia7_clipmagazinehole_0_1", - "sample_id": null, - "children": [ - "zi_dan_jia7_jipian_1" - ], - "parent": "zi_dan_jia7", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia7_jipian_1", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia7_clipmagazinehole_0_1" - } - ] - } - }, - { - "id": "zi_dan_jia7_jipian_1", - "name": "zi_dan_jia7_jipian_1", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia7_clipmagazinehole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia7_clipmagazinehole_1_0", - "name": "zi_dan_jia7_clipmagazinehole_1_0", - "sample_id": null, - "children": [ - "zi_dan_jia7_jipian_2" - ], - "parent": "zi_dan_jia7", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia7_jipian_2", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia7_clipmagazinehole_1_0" - } - ] - } - }, - { - "id": "zi_dan_jia7_jipian_2", - "name": "zi_dan_jia7_jipian_2", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia7_clipmagazinehole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia7_clipmagazinehole_1_1", - "name": "zi_dan_jia7_clipmagazinehole_1_1", - "sample_id": null, - "children": [ - "zi_dan_jia7_jipian_3" - ], - "parent": "zi_dan_jia7", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia7_jipian_3", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia7_clipmagazinehole_1_1" - } - ] - } - }, - { - "id": "zi_dan_jia7_jipian_3", - "name": "zi_dan_jia7_jipian_3", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia7_clipmagazinehole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia7_clipmagazinehole_2_0", - "name": "zi_dan_jia7_clipmagazinehole_2_0", - "sample_id": null, - "children": [ - "zi_dan_jia7_jipian_4" - ], - "parent": "zi_dan_jia7", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia7_jipian_4", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia7_clipmagazinehole_2_0" - } - ] - } - }, - { - "id": "zi_dan_jia7_jipian_4", - "name": "zi_dan_jia7_jipian_4", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia7_clipmagazinehole_2_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia7_clipmagazinehole_2_1", - "name": "zi_dan_jia7_clipmagazinehole_2_1", - "sample_id": null, - "children": [ - "zi_dan_jia7_jipian_5" - ], - "parent": "zi_dan_jia7", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia7_jipian_5", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia7_clipmagazinehole_2_1" - } - ] - } - }, - { - "id": "zi_dan_jia7_jipian_5", - "name": "zi_dan_jia7_jipian_5", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia7_clipmagazinehole_2_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia8", - "name": "zi_dan_jia8", - "sample_id": null, - "children": [ - "zi_dan_jia8_clipmagazinehole_0_0", - "zi_dan_jia8_clipmagazinehole_0_1", - "zi_dan_jia8_clipmagazinehole_1_0", - "zi_dan_jia8_clipmagazinehole_1_1", - "zi_dan_jia8_clipmagazinehole_2_0", - "zi_dan_jia8_clipmagazinehole_2_1" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1280, - "y": 400, - "z": 0 - }, - "config": { - "type": "ClipMagazine", - "size_x": 80, - "size_y": 80, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine", - "model": null, - "barcode": null, - "ordering": { - "A1": "zi_dan_jia8_clipmagazinehole_0_0", - "B1": "zi_dan_jia8_clipmagazinehole_0_1", - "A2": "zi_dan_jia8_clipmagazinehole_1_0", - "B2": "zi_dan_jia8_clipmagazinehole_1_1", - "A3": "zi_dan_jia8_clipmagazinehole_2_0", - "B3": "zi_dan_jia8_clipmagazinehole_2_1" - }, - "hole_diameter": 14.0, - "hole_depth": 10.0, - "max_sheets_per_hole": 100 - }, - "data": {} - }, - { - "id": "zi_dan_jia8_clipmagazinehole_0_0", - "name": "zi_dan_jia8_clipmagazinehole_0_0", - "sample_id": null, - "children": [ - "zi_dan_jia8_jipian_0" - ], - "parent": "zi_dan_jia8", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia8_jipian_0", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia8_clipmagazinehole_0_0" - } - ] - } - }, - { - "id": "zi_dan_jia8_jipian_0", - "name": "zi_dan_jia8_jipian_0", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia8_clipmagazinehole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia8_clipmagazinehole_0_1", - "name": "zi_dan_jia8_clipmagazinehole_0_1", - "sample_id": null, - "children": [ - "zi_dan_jia8_jipian_1" - ], - "parent": "zi_dan_jia8", - "type": "container", - "class": "", - "position": { - "x": 15.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia8_jipian_1", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia8_clipmagazinehole_0_1" - } - ] - } - }, - { - "id": "zi_dan_jia8_jipian_1", - "name": "zi_dan_jia8_jipian_1", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia8_clipmagazinehole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia8_clipmagazinehole_1_0", - "name": "zi_dan_jia8_clipmagazinehole_1_0", - "sample_id": null, - "children": [ - "zi_dan_jia8_jipian_2" - ], - "parent": "zi_dan_jia8", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia8_jipian_2", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia8_clipmagazinehole_1_0" - } - ] - } - }, - { - "id": "zi_dan_jia8_jipian_2", - "name": "zi_dan_jia8_jipian_2", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia8_clipmagazinehole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia8_clipmagazinehole_1_1", - "name": "zi_dan_jia8_clipmagazinehole_1_1", - "sample_id": null, - "children": [ - "zi_dan_jia8_jipian_3" - ], - "parent": "zi_dan_jia8", - "type": "container", - "class": "", - "position": { - "x": 40.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia8_jipian_3", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia8_clipmagazinehole_1_1" - } - ] - } - }, - { - "id": "zi_dan_jia8_jipian_3", - "name": "zi_dan_jia8_jipian_3", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia8_clipmagazinehole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia8_clipmagazinehole_2_0", - "name": "zi_dan_jia8_clipmagazinehole_2_0", - "sample_id": null, - "children": [ - "zi_dan_jia8_jipian_4" - ], - "parent": "zi_dan_jia8", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 52.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia8_jipian_4", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia8_clipmagazinehole_2_0" - } - ] - } - }, - { - "id": "zi_dan_jia8_jipian_4", - "name": "zi_dan_jia8_jipian_4", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia8_clipmagazinehole_2_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "zi_dan_jia8_clipmagazinehole_2_1", - "name": "zi_dan_jia8_clipmagazinehole_2_1", - "sample_id": null, - "children": [ - "zi_dan_jia8_jipian_5" - ], - "parent": "zi_dan_jia8", - "type": "container", - "class": "", - "position": { - "x": 65.0, - "y": 27.5, - "z": 10 - }, - "config": { - "type": "ClipMagazineHole", - "size_x": 14.0, - "size_y": 14.0, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "clip_magazine_hole", - "model": null, - "barcode": null, - "max_volume": 1960.0, - "material_z_thickness": null, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "sheet_count": 1, - "sheets": [ - { - "name": "zi_dan_jia8_jipian_5", - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "location": null, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null, - "children": [], - "parent_name": "zi_dan_jia8_clipmagazinehole_2_1" - } - ] - } - }, - { - "id": "zi_dan_jia8_jipian_5", - "name": "zi_dan_jia8_jipian_5", - "sample_id": null, - "children": [], - "parent": "zi_dan_jia8_clipmagazinehole_2_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1", - "name": "liaopan1", - "sample_id": null, - "children": [ - "liaopan1_materialhole_0_0", - "liaopan1_materialhole_0_1", - "liaopan1_materialhole_0_2", - "liaopan1_materialhole_0_3", - "liaopan1_materialhole_1_0", - "liaopan1_materialhole_1_1", - "liaopan1_materialhole_1_2", - "liaopan1_materialhole_1_3", - "liaopan1_materialhole_2_0", - "liaopan1_materialhole_2_1", - "liaopan1_materialhole_2_2", - "liaopan1_materialhole_2_3", - "liaopan1_materialhole_3_0", - "liaopan1_materialhole_3_1", - "liaopan1_materialhole_3_2", - "liaopan1_materialhole_3_3" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1010, - "y": 50, - "z": 0 - }, - "config": { - "type": "MaterialPlate", - "size_x": 120, - "size_y": 100, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_plate", - "model": null, - "barcode": null, - "ordering": { - "A1": "liaopan1_materialhole_0_0", - "B1": "liaopan1_materialhole_0_1", - "C1": "liaopan1_materialhole_0_2", - "D1": "liaopan1_materialhole_0_3", - "A2": "liaopan1_materialhole_1_0", - "B2": "liaopan1_materialhole_1_1", - "C2": "liaopan1_materialhole_1_2", - "D2": "liaopan1_materialhole_1_3", - "A3": "liaopan1_materialhole_2_0", - "B3": "liaopan1_materialhole_2_1", - "C3": "liaopan1_materialhole_2_2", - "D3": "liaopan1_materialhole_2_3", - "A4": "liaopan1_materialhole_3_0", - "B4": "liaopan1_materialhole_3_1", - "C4": "liaopan1_materialhole_3_2", - "D4": "liaopan1_materialhole_3_3" - } - }, - "data": {} - }, - { - "id": "liaopan1_materialhole_0_0", - "name": "liaopan1_materialhole_0_0", - "sample_id": null, - "children": [ - "liaopan1_jipian_0" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_0", - "name": "liaopan1_jipian_0", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_0_1", - "name": "liaopan1_materialhole_0_1", - "sample_id": null, - "children": [ - "liaopan1_jipian_1" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_1", - "name": "liaopan1_jipian_1", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_0_2", - "name": "liaopan1_materialhole_0_2", - "sample_id": null, - "children": [ - "liaopan1_jipian_2" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_2", - "name": "liaopan1_jipian_2", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_0_2", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_0_3", - "name": "liaopan1_materialhole_0_3", - "sample_id": null, - "children": [ - "liaopan1_jipian_3" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_3", - "name": "liaopan1_jipian_3", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_0_3", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_1_0", - "name": "liaopan1_materialhole_1_0", - "sample_id": null, - "children": [ - "liaopan1_jipian_4" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_4", - "name": "liaopan1_jipian_4", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_1_1", - "name": "liaopan1_materialhole_1_1", - "sample_id": null, - "children": [ - "liaopan1_jipian_5" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_5", - "name": "liaopan1_jipian_5", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_1_2", - "name": "liaopan1_materialhole_1_2", - "sample_id": null, - "children": [ - "liaopan1_jipian_6" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_6", - "name": "liaopan1_jipian_6", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_1_2", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_1_3", - "name": "liaopan1_materialhole_1_3", - "sample_id": null, - "children": [ - "liaopan1_jipian_7" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_7", - "name": "liaopan1_jipian_7", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_1_3", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_2_0", - "name": "liaopan1_materialhole_2_0", - "sample_id": null, - "children": [ - "liaopan1_jipian_8" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_8", - "name": "liaopan1_jipian_8", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_2_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_2_1", - "name": "liaopan1_materialhole_2_1", - "sample_id": null, - "children": [ - "liaopan1_jipian_9" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_9", - "name": "liaopan1_jipian_9", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_2_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_2_2", - "name": "liaopan1_materialhole_2_2", - "sample_id": null, - "children": [ - "liaopan1_jipian_10" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_10", - "name": "liaopan1_jipian_10", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_2_2", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_2_3", - "name": "liaopan1_materialhole_2_3", - "sample_id": null, - "children": [ - "liaopan1_jipian_11" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_11", - "name": "liaopan1_jipian_11", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_2_3", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_3_0", - "name": "liaopan1_materialhole_3_0", - "sample_id": null, - "children": [ - "liaopan1_jipian_12" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_12", - "name": "liaopan1_jipian_12", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_3_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_3_1", - "name": "liaopan1_materialhole_3_1", - "sample_id": null, - "children": [ - "liaopan1_jipian_13" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_13", - "name": "liaopan1_jipian_13", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_3_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_3_2", - "name": "liaopan1_materialhole_3_2", - "sample_id": null, - "children": [ - "liaopan1_jipian_14" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_14", - "name": "liaopan1_jipian_14", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_3_2", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan1_materialhole_3_3", - "name": "liaopan1_materialhole_3_3", - "sample_id": null, - "children": [ - "liaopan1_jipian_15" - ], - "parent": "liaopan1", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan1_jipian_15", - "name": "liaopan1_jipian_15", - "sample_id": null, - "children": [], - "parent": "liaopan1_materialhole_3_3", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan2", - "name": "liaopan2", - "sample_id": null, - "children": [ - "liaopan2_materialhole_0_0", - "liaopan2_materialhole_0_1", - "liaopan2_materialhole_0_2", - "liaopan2_materialhole_0_3", - "liaopan2_materialhole_1_0", - "liaopan2_materialhole_1_1", - "liaopan2_materialhole_1_2", - "liaopan2_materialhole_1_3", - "liaopan2_materialhole_2_0", - "liaopan2_materialhole_2_1", - "liaopan2_materialhole_2_2", - "liaopan2_materialhole_2_3", - "liaopan2_materialhole_3_0", - "liaopan2_materialhole_3_1", - "liaopan2_materialhole_3_2", - "liaopan2_materialhole_3_3" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1130, - "y": 50, - "z": 0 - }, - "config": { - "type": "MaterialPlate", - "size_x": 120, - "size_y": 100, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_plate", - "model": null, - "barcode": null, - "ordering": { - "A1": "liaopan2_materialhole_0_0", - "B1": "liaopan2_materialhole_0_1", - "C1": "liaopan2_materialhole_0_2", - "D1": "liaopan2_materialhole_0_3", - "A2": "liaopan2_materialhole_1_0", - "B2": "liaopan2_materialhole_1_1", - "C2": "liaopan2_materialhole_1_2", - "D2": "liaopan2_materialhole_1_3", - "A3": "liaopan2_materialhole_2_0", - "B3": "liaopan2_materialhole_2_1", - "C3": "liaopan2_materialhole_2_2", - "D3": "liaopan2_materialhole_2_3", - "A4": "liaopan2_materialhole_3_0", - "B4": "liaopan2_materialhole_3_1", - "C4": "liaopan2_materialhole_3_2", - "D4": "liaopan2_materialhole_3_3" - } - }, - "data": {} - }, - { - "id": "liaopan2_materialhole_0_0", - "name": "liaopan2_materialhole_0_0", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_0_1", - "name": "liaopan2_materialhole_0_1", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_0_2", - "name": "liaopan2_materialhole_0_2", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_0_3", - "name": "liaopan2_materialhole_0_3", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_1_0", - "name": "liaopan2_materialhole_1_0", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_1_1", - "name": "liaopan2_materialhole_1_1", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_1_2", - "name": "liaopan2_materialhole_1_2", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_1_3", - "name": "liaopan2_materialhole_1_3", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_2_0", - "name": "liaopan2_materialhole_2_0", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_2_1", - "name": "liaopan2_materialhole_2_1", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_2_2", - "name": "liaopan2_materialhole_2_2", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_2_3", - "name": "liaopan2_materialhole_2_3", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_3_0", - "name": "liaopan2_materialhole_3_0", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_3_1", - "name": "liaopan2_materialhole_3_1", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_3_2", - "name": "liaopan2_materialhole_3_2", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan2_materialhole_3_3", - "name": "liaopan2_materialhole_3_3", - "sample_id": null, - "children": [], - "parent": "liaopan2", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3", - "name": "liaopan3", - "sample_id": null, - "children": [ - "liaopan3_materialhole_0_0", - "liaopan3_materialhole_0_1", - "liaopan3_materialhole_0_2", - "liaopan3_materialhole_0_3", - "liaopan3_materialhole_1_0", - "liaopan3_materialhole_1_1", - "liaopan3_materialhole_1_2", - "liaopan3_materialhole_1_3", - "liaopan3_materialhole_2_0", - "liaopan3_materialhole_2_1", - "liaopan3_materialhole_2_2", - "liaopan3_materialhole_2_3", - "liaopan3_materialhole_3_0", - "liaopan3_materialhole_3_1", - "liaopan3_materialhole_3_2", - "liaopan3_materialhole_3_3" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1250, - "y": 50, - "z": 0 - }, - "config": { - "type": "MaterialPlate", - "size_x": 120, - "size_y": 100, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_plate", - "model": null, - "barcode": null, - "ordering": { - "A1": "liaopan3_materialhole_0_0", - "B1": "liaopan3_materialhole_0_1", - "C1": "liaopan3_materialhole_0_2", - "D1": "liaopan3_materialhole_0_3", - "A2": "liaopan3_materialhole_1_0", - "B2": "liaopan3_materialhole_1_1", - "C2": "liaopan3_materialhole_1_2", - "D2": "liaopan3_materialhole_1_3", - "A3": "liaopan3_materialhole_2_0", - "B3": "liaopan3_materialhole_2_1", - "C3": "liaopan3_materialhole_2_2", - "D3": "liaopan3_materialhole_2_3", - "A4": "liaopan3_materialhole_3_0", - "B4": "liaopan3_materialhole_3_1", - "C4": "liaopan3_materialhole_3_2", - "D4": "liaopan3_materialhole_3_3" - } - }, - "data": {} - }, - { - "id": "liaopan3_materialhole_0_0", - "name": "liaopan3_materialhole_0_0", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_0_1", - "name": "liaopan3_materialhole_0_1", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_0_2", - "name": "liaopan3_materialhole_0_2", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_0_3", - "name": "liaopan3_materialhole_0_3", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_1_0", - "name": "liaopan3_materialhole_1_0", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_1_1", - "name": "liaopan3_materialhole_1_1", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_1_2", - "name": "liaopan3_materialhole_1_2", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_1_3", - "name": "liaopan3_materialhole_1_3", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_2_0", - "name": "liaopan3_materialhole_2_0", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_2_1", - "name": "liaopan3_materialhole_2_1", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_2_2", - "name": "liaopan3_materialhole_2_2", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_2_3", - "name": "liaopan3_materialhole_2_3", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_3_0", - "name": "liaopan3_materialhole_3_0", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_3_1", - "name": "liaopan3_materialhole_3_1", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_3_2", - "name": "liaopan3_materialhole_3_2", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan3_materialhole_3_3", - "name": "liaopan3_materialhole_3_3", - "sample_id": null, - "children": [], - "parent": "liaopan3", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4", - "name": "liaopan4", - "sample_id": null, - "children": [ - "liaopan4_materialhole_0_0", - "liaopan4_materialhole_0_1", - "liaopan4_materialhole_0_2", - "liaopan4_materialhole_0_3", - "liaopan4_materialhole_1_0", - "liaopan4_materialhole_1_1", - "liaopan4_materialhole_1_2", - "liaopan4_materialhole_1_3", - "liaopan4_materialhole_2_0", - "liaopan4_materialhole_2_1", - "liaopan4_materialhole_2_2", - "liaopan4_materialhole_2_3", - "liaopan4_materialhole_3_0", - "liaopan4_materialhole_3_1", - "liaopan4_materialhole_3_2", - "liaopan4_materialhole_3_3" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1010, - "y": 150, - "z": 0 - }, - "config": { - "type": "MaterialPlate", - "size_x": 120, - "size_y": 100, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_plate", - "model": null, - "barcode": null, - "ordering": { - "A1": "liaopan4_materialhole_0_0", - "B1": "liaopan4_materialhole_0_1", - "C1": "liaopan4_materialhole_0_2", - "D1": "liaopan4_materialhole_0_3", - "A2": "liaopan4_materialhole_1_0", - "B2": "liaopan4_materialhole_1_1", - "C2": "liaopan4_materialhole_1_2", - "D2": "liaopan4_materialhole_1_3", - "A3": "liaopan4_materialhole_2_0", - "B3": "liaopan4_materialhole_2_1", - "C3": "liaopan4_materialhole_2_2", - "D3": "liaopan4_materialhole_2_3", - "A4": "liaopan4_materialhole_3_0", - "B4": "liaopan4_materialhole_3_1", - "C4": "liaopan4_materialhole_3_2", - "D4": "liaopan4_materialhole_3_3" - } - }, - "data": {} - }, - { - "id": "liaopan4_materialhole_0_0", - "name": "liaopan4_materialhole_0_0", - "sample_id": null, - "children": [ - "liaopan4_jipian_0" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_0", - "name": "liaopan4_jipian_0", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_0_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_0_1", - "name": "liaopan4_materialhole_0_1", - "sample_id": null, - "children": [ - "liaopan4_jipian_1" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_1", - "name": "liaopan4_jipian_1", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_0_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_0_2", - "name": "liaopan4_materialhole_0_2", - "sample_id": null, - "children": [ - "liaopan4_jipian_2" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_2", - "name": "liaopan4_jipian_2", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_0_2", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_0_3", - "name": "liaopan4_materialhole_0_3", - "sample_id": null, - "children": [ - "liaopan4_jipian_3" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_3", - "name": "liaopan4_jipian_3", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_0_3", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_1_0", - "name": "liaopan4_materialhole_1_0", - "sample_id": null, - "children": [ - "liaopan4_jipian_4" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_4", - "name": "liaopan4_jipian_4", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_1_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_1_1", - "name": "liaopan4_materialhole_1_1", - "sample_id": null, - "children": [ - "liaopan4_jipian_5" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_5", - "name": "liaopan4_jipian_5", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_1_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_1_2", - "name": "liaopan4_materialhole_1_2", - "sample_id": null, - "children": [ - "liaopan4_jipian_6" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_6", - "name": "liaopan4_jipian_6", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_1_2", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_1_3", - "name": "liaopan4_materialhole_1_3", - "sample_id": null, - "children": [ - "liaopan4_jipian_7" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_7", - "name": "liaopan4_jipian_7", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_1_3", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_2_0", - "name": "liaopan4_materialhole_2_0", - "sample_id": null, - "children": [ - "liaopan4_jipian_8" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_8", - "name": "liaopan4_jipian_8", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_2_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_2_1", - "name": "liaopan4_materialhole_2_1", - "sample_id": null, - "children": [ - "liaopan4_jipian_9" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_9", - "name": "liaopan4_jipian_9", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_2_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_2_2", - "name": "liaopan4_materialhole_2_2", - "sample_id": null, - "children": [ - "liaopan4_jipian_10" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_10", - "name": "liaopan4_jipian_10", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_2_2", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_2_3", - "name": "liaopan4_materialhole_2_3", - "sample_id": null, - "children": [ - "liaopan4_jipian_11" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_11", - "name": "liaopan4_jipian_11", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_2_3", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_3_0", - "name": "liaopan4_materialhole_3_0", - "sample_id": null, - "children": [ - "liaopan4_jipian_12" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_12", - "name": "liaopan4_jipian_12", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_3_0", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_3_1", - "name": "liaopan4_materialhole_3_1", - "sample_id": null, - "children": [ - "liaopan4_jipian_13" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_13", - "name": "liaopan4_jipian_13", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_3_1", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_3_2", - "name": "liaopan4_materialhole_3_2", - "sample_id": null, - "children": [ - "liaopan4_jipian_14" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_14", - "name": "liaopan4_jipian_14", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_3_2", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan4_materialhole_3_3", - "name": "liaopan4_materialhole_3_3", - "sample_id": null, - "children": [ - "liaopan4_jipian_15" - ], - "parent": "liaopan4", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan4_jipian_15", - "name": "liaopan4_jipian_15", - "sample_id": null, - "children": [], - "parent": "liaopan4_materialhole_3_3", - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "liaopan5", - "name": "liaopan5", - "sample_id": null, - "children": [ - "liaopan5_materialhole_0_0", - "liaopan5_materialhole_0_1", - "liaopan5_materialhole_0_2", - "liaopan5_materialhole_0_3", - "liaopan5_materialhole_1_0", - "liaopan5_materialhole_1_1", - "liaopan5_materialhole_1_2", - "liaopan5_materialhole_1_3", - "liaopan5_materialhole_2_0", - "liaopan5_materialhole_2_1", - "liaopan5_materialhole_2_2", - "liaopan5_materialhole_2_3", - "liaopan5_materialhole_3_0", - "liaopan5_materialhole_3_1", - "liaopan5_materialhole_3_2", - "liaopan5_materialhole_3_3" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1130, - "y": 150, - "z": 0 - }, - "config": { - "type": "MaterialPlate", - "size_x": 120, - "size_y": 100, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_plate", - "model": null, - "barcode": null, - "ordering": { - "A1": "liaopan5_materialhole_0_0", - "B1": "liaopan5_materialhole_0_1", - "C1": "liaopan5_materialhole_0_2", - "D1": "liaopan5_materialhole_0_3", - "A2": "liaopan5_materialhole_1_0", - "B2": "liaopan5_materialhole_1_1", - "C2": "liaopan5_materialhole_1_2", - "D2": "liaopan5_materialhole_1_3", - "A3": "liaopan5_materialhole_2_0", - "B3": "liaopan5_materialhole_2_1", - "C3": "liaopan5_materialhole_2_2", - "D3": "liaopan5_materialhole_2_3", - "A4": "liaopan5_materialhole_3_0", - "B4": "liaopan5_materialhole_3_1", - "C4": "liaopan5_materialhole_3_2", - "D4": "liaopan5_materialhole_3_3" - } - }, - "data": {} - }, - { - "id": "liaopan5_materialhole_0_0", - "name": "liaopan5_materialhole_0_0", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_0_1", - "name": "liaopan5_materialhole_0_1", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_0_2", - "name": "liaopan5_materialhole_0_2", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_0_3", - "name": "liaopan5_materialhole_0_3", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_1_0", - "name": "liaopan5_materialhole_1_0", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_1_1", - "name": "liaopan5_materialhole_1_1", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_1_2", - "name": "liaopan5_materialhole_1_2", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_1_3", - "name": "liaopan5_materialhole_1_3", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_2_0", - "name": "liaopan5_materialhole_2_0", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_2_1", - "name": "liaopan5_materialhole_2_1", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_2_2", - "name": "liaopan5_materialhole_2_2", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_2_3", - "name": "liaopan5_materialhole_2_3", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_3_0", - "name": "liaopan5_materialhole_3_0", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_3_1", - "name": "liaopan5_materialhole_3_1", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_3_2", - "name": "liaopan5_materialhole_3_2", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan5_materialhole_3_3", - "name": "liaopan5_materialhole_3_3", - "sample_id": null, - "children": [], - "parent": "liaopan5", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6", - "name": "liaopan6", - "sample_id": null, - "children": [ - "liaopan6_materialhole_0_0", - "liaopan6_materialhole_0_1", - "liaopan6_materialhole_0_2", - "liaopan6_materialhole_0_3", - "liaopan6_materialhole_1_0", - "liaopan6_materialhole_1_1", - "liaopan6_materialhole_1_2", - "liaopan6_materialhole_1_3", - "liaopan6_materialhole_2_0", - "liaopan6_materialhole_2_1", - "liaopan6_materialhole_2_2", - "liaopan6_materialhole_2_3", - "liaopan6_materialhole_3_0", - "liaopan6_materialhole_3_1", - "liaopan6_materialhole_3_2", - "liaopan6_materialhole_3_3" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 1250, - "y": 150, - "z": 0 - }, - "config": { - "type": "MaterialPlate", - "size_x": 120, - "size_y": 100, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_plate", - "model": null, - "barcode": null, - "ordering": { - "A1": "liaopan6_materialhole_0_0", - "B1": "liaopan6_materialhole_0_1", - "C1": "liaopan6_materialhole_0_2", - "D1": "liaopan6_materialhole_0_3", - "A2": "liaopan6_materialhole_1_0", - "B2": "liaopan6_materialhole_1_1", - "C2": "liaopan6_materialhole_1_2", - "D2": "liaopan6_materialhole_1_3", - "A3": "liaopan6_materialhole_2_0", - "B3": "liaopan6_materialhole_2_1", - "C3": "liaopan6_materialhole_2_2", - "D3": "liaopan6_materialhole_2_3", - "A4": "liaopan6_materialhole_3_0", - "B4": "liaopan6_materialhole_3_1", - "C4": "liaopan6_materialhole_3_2", - "D4": "liaopan6_materialhole_3_3" - } - }, - "data": {} - }, - { - "id": "liaopan6_materialhole_0_0", - "name": "liaopan6_materialhole_0_0", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_0_1", - "name": "liaopan6_materialhole_0_1", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_0_2", - "name": "liaopan6_materialhole_0_2", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_0_3", - "name": "liaopan6_materialhole_0_3", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 12.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_1_0", - "name": "liaopan6_materialhole_1_0", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_1_1", - "name": "liaopan6_materialhole_1_1", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_1_2", - "name": "liaopan6_materialhole_1_2", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_1_3", - "name": "liaopan6_materialhole_1_3", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 36.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_2_0", - "name": "liaopan6_materialhole_2_0", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_2_1", - "name": "liaopan6_materialhole_2_1", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_2_2", - "name": "liaopan6_materialhole_2_2", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_2_3", - "name": "liaopan6_materialhole_2_3", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 60.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_3_0", - "name": "liaopan6_materialhole_3_0", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 74.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_3_1", - "name": "liaopan6_materialhole_3_1", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 50.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_3_2", - "name": "liaopan6_materialhole_3_2", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 26.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "liaopan6_materialhole_3_3", - "name": "liaopan6_materialhole_3_3", - "sample_id": null, - "children": [], - "parent": "liaopan6", - "type": "container", - "class": "", - "position": { - "x": 84.0, - "y": 2.0, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "bottle_rack_3x4", - "name": "bottle_rack_3x4", - "sample_id": null, - "children": [ - "sheet_3x4_0", - "sheet_3x4_1", - "sheet_3x4_2", - "sheet_3x4_3", - "sheet_3x4_4", - "sheet_3x4_5", - "sheet_3x4_6", - "sheet_3x4_7", - "sheet_3x4_8", - "sheet_3x4_9", - "sheet_3x4_10", - "sheet_3x4_11" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 100, - "y": 200, - "z": 0 - }, - "config": { - "type": "BottleRack", - "size_x": 210.0, - "size_y": 140.0, - "size_z": 100.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "bottle_rack", - "model": null, - "barcode": null, - "num_items_x": 3, - "num_items_y": 4, - "position_spacing": 35.0, - "orientation": "vertical", - "padding_x": 20.0, - "padding_y": 20.0 - }, - "data": { - "bottle_diameter": 30.0, - "bottle_height": 100.0, - "position_spacing": 35.0, - "name_to_index": {} - } - }, - { - "id": "sheet_3x4_0", - "name": "sheet_3x4_0", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 20.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_1", - "name": "sheet_3x4_1", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 55.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_2", - "name": "sheet_3x4_2", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 90.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_3", - "name": "sheet_3x4_3", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 20.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_4", - "name": "sheet_3x4_4", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 55.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_5", - "name": "sheet_3x4_5", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 90.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_6", - "name": "sheet_3x4_6", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 90.0, - "y": 20.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_7", - "name": "sheet_3x4_7", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 90.0, - "y": 55.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_8", - "name": "sheet_3x4_8", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 90.0, - "y": 90.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_9", - "name": "sheet_3x4_9", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 125.0, - "y": 20.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_10", - "name": "sheet_3x4_10", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 125.0, - "y": 55.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_3x4_11", - "name": "sheet_3x4_11", - "sample_id": null, - "children": [], - "parent": "bottle_rack_3x4", - "type": "container", - "class": "", - "position": { - "x": 125.0, - "y": 90.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "bottle_rack_6x2", - "name": "bottle_rack_6x2", - "sample_id": null, - "children": [ - "sheet_6x2_0", - "sheet_6x2_1", - "sheet_6x2_2", - "sheet_6x2_3", - "sheet_6x2_4", - "sheet_6x2_5", - "sheet_6x2_6", - "sheet_6x2_7", - "sheet_6x2_8", - "sheet_6x2_9", - "sheet_6x2_10", - "sheet_6x2_11" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 300, - "y": 300, - "z": 0 - }, - "config": { - "type": "BottleRack", - "size_x": 120.0, - "size_y": 250.0, - "size_z": 100.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "bottle_rack", - "model": null, - "barcode": null, - "num_items_x": 6, - "num_items_y": 2, - "position_spacing": 35.0, - "orientation": "vertical", - "padding_x": 20.0, - "padding_y": 20.0 - }, - "data": { - "bottle_diameter": 30.0, - "bottle_height": 100.0, - "position_spacing": 35.0, - "name_to_index": {} - } - }, - { - "id": "sheet_6x2_0", - "name": "sheet_6x2_0", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 20.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_1", - "name": "sheet_6x2_1", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 55.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_2", - "name": "sheet_6x2_2", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 90.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_3", - "name": "sheet_6x2_3", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 125.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_4", - "name": "sheet_6x2_4", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 160.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_5", - "name": "sheet_6x2_5", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 20.0, - "y": 195.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_6", - "name": "sheet_6x2_6", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 20.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_7", - "name": "sheet_6x2_7", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 55.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_8", - "name": "sheet_6x2_8", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 90.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_9", - "name": "sheet_6x2_9", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 125.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_10", - "name": "sheet_6x2_10", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 160.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "sheet_6x2_11", - "name": "sheet_6x2_11", - "sample_id": null, - "children": [], - "parent": "bottle_rack_6x2", - "type": "container", - "class": "", - "position": { - "x": 55.0, - "y": 195.0, - "z": 0 - }, - "config": { - "type": "ElectrodeSheet", - "size_x": 12, - "size_y": 12, - "size_z": 0.1, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "electrode_sheet", - "model": null, - "barcode": null - }, - "data": { - "diameter": 14, - "thickness": 0.1, - "mass": 0.5, - "material_type": "copper", - "info": null - } - }, - { - "id": "bottle_rack_6x2_2", - "name": "bottle_rack_6x2_2", - "sample_id": null, - "children": [], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 430, - "y": 300, - "z": 0 - }, - "config": { - "type": "BottleRack", - "size_x": 120.0, - "size_y": 250.0, - "size_z": 100.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "bottle_rack", - "model": null, - "barcode": null, - "num_items_x": 6, - "num_items_y": 2, - "position_spacing": 35.0, - "orientation": "vertical", - "padding_x": 20.0, - "padding_y": 20.0 - }, - "data": { - "bottle_diameter": 30.0, - "bottle_height": 100.0, - "position_spacing": 35.0, - "name_to_index": {} - } - }, - { - "id": "tip_box_64", - "name": "tip_box_64", - "sample_id": null, - "children": [ - "tip_box_64_tipspot_0_0", - "tip_box_64_tipspot_0_1", - "tip_box_64_tipspot_0_2", - "tip_box_64_tipspot_0_3", - "tip_box_64_tipspot_0_4", - "tip_box_64_tipspot_0_5", - "tip_box_64_tipspot_0_6", - "tip_box_64_tipspot_0_7", - "tip_box_64_tipspot_1_0", - "tip_box_64_tipspot_1_1", - "tip_box_64_tipspot_1_2", - "tip_box_64_tipspot_1_3", - "tip_box_64_tipspot_1_4", - "tip_box_64_tipspot_1_5", - "tip_box_64_tipspot_1_6", - "tip_box_64_tipspot_1_7", - "tip_box_64_tipspot_2_0", - "tip_box_64_tipspot_2_1", - "tip_box_64_tipspot_2_2", - "tip_box_64_tipspot_2_3", - "tip_box_64_tipspot_2_4", - "tip_box_64_tipspot_2_5", - "tip_box_64_tipspot_2_6", - "tip_box_64_tipspot_2_7", - "tip_box_64_tipspot_3_0", - "tip_box_64_tipspot_3_1", - "tip_box_64_tipspot_3_2", - "tip_box_64_tipspot_3_3", - "tip_box_64_tipspot_3_4", - "tip_box_64_tipspot_3_5", - "tip_box_64_tipspot_3_6", - "tip_box_64_tipspot_3_7", - "tip_box_64_tipspot_4_0", - "tip_box_64_tipspot_4_1", - "tip_box_64_tipspot_4_2", - "tip_box_64_tipspot_4_3", - "tip_box_64_tipspot_4_4", - "tip_box_64_tipspot_4_5", - "tip_box_64_tipspot_4_6", - "tip_box_64_tipspot_4_7", - "tip_box_64_tipspot_5_0", - "tip_box_64_tipspot_5_1", - "tip_box_64_tipspot_5_2", - "tip_box_64_tipspot_5_3", - "tip_box_64_tipspot_5_4", - "tip_box_64_tipspot_5_5", - "tip_box_64_tipspot_5_6", - "tip_box_64_tipspot_5_7", - "tip_box_64_tipspot_6_0", - "tip_box_64_tipspot_6_1", - "tip_box_64_tipspot_6_2", - "tip_box_64_tipspot_6_3", - "tip_box_64_tipspot_6_4", - "tip_box_64_tipspot_6_5", - "tip_box_64_tipspot_6_6", - "tip_box_64_tipspot_6_7", - "tip_box_64_tipspot_7_0", - "tip_box_64_tipspot_7_1", - "tip_box_64_tipspot_7_2", - "tip_box_64_tipspot_7_3", - "tip_box_64_tipspot_7_4", - "tip_box_64_tipspot_7_5", - "tip_box_64_tipspot_7_6", - "tip_box_64_tipspot_7_7" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 300, - "y": 100, - "z": 0 - }, - "config": { - "type": "TipBox64", - "size_x": 127.8, - "size_y": 85.5, - "size_z": 60.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_box_64", - "model": null, - "barcode": null, - "ordering": { - "A1": "tip_box_64_tipspot_0_0", - "B1": "tip_box_64_tipspot_0_1", - "C1": "tip_box_64_tipspot_0_2", - "D1": "tip_box_64_tipspot_0_3", - "E1": "tip_box_64_tipspot_0_4", - "F1": "tip_box_64_tipspot_0_5", - "G1": "tip_box_64_tipspot_0_6", - "H1": "tip_box_64_tipspot_0_7", - "A2": "tip_box_64_tipspot_1_0", - "B2": "tip_box_64_tipspot_1_1", - "C2": "tip_box_64_tipspot_1_2", - "D2": "tip_box_64_tipspot_1_3", - "E2": "tip_box_64_tipspot_1_4", - "F2": "tip_box_64_tipspot_1_5", - "G2": "tip_box_64_tipspot_1_6", - "H2": "tip_box_64_tipspot_1_7", - "A3": "tip_box_64_tipspot_2_0", - "B3": "tip_box_64_tipspot_2_1", - "C3": "tip_box_64_tipspot_2_2", - "D3": "tip_box_64_tipspot_2_3", - "E3": "tip_box_64_tipspot_2_4", - "F3": "tip_box_64_tipspot_2_5", - "G3": "tip_box_64_tipspot_2_6", - "H3": "tip_box_64_tipspot_2_7", - "A4": "tip_box_64_tipspot_3_0", - "B4": "tip_box_64_tipspot_3_1", - "C4": "tip_box_64_tipspot_3_2", - "D4": "tip_box_64_tipspot_3_3", - "E4": "tip_box_64_tipspot_3_4", - "F4": "tip_box_64_tipspot_3_5", - "G4": "tip_box_64_tipspot_3_6", - "H4": "tip_box_64_tipspot_3_7", - "A5": "tip_box_64_tipspot_4_0", - "B5": "tip_box_64_tipspot_4_1", - "C5": "tip_box_64_tipspot_4_2", - "D5": "tip_box_64_tipspot_4_3", - "E5": "tip_box_64_tipspot_4_4", - "F5": "tip_box_64_tipspot_4_5", - "G5": "tip_box_64_tipspot_4_6", - "H5": "tip_box_64_tipspot_4_7", - "A6": "tip_box_64_tipspot_5_0", - "B6": "tip_box_64_tipspot_5_1", - "C6": "tip_box_64_tipspot_5_2", - "D6": "tip_box_64_tipspot_5_3", - "E6": "tip_box_64_tipspot_5_4", - "F6": "tip_box_64_tipspot_5_5", - "G6": "tip_box_64_tipspot_5_6", - "H6": "tip_box_64_tipspot_5_7", - "A7": "tip_box_64_tipspot_6_0", - "B7": "tip_box_64_tipspot_6_1", - "C7": "tip_box_64_tipspot_6_2", - "D7": "tip_box_64_tipspot_6_3", - "E7": "tip_box_64_tipspot_6_4", - "F7": "tip_box_64_tipspot_6_5", - "G7": "tip_box_64_tipspot_6_6", - "H7": "tip_box_64_tipspot_6_7", - "A8": "tip_box_64_tipspot_7_0", - "B8": "tip_box_64_tipspot_7_1", - "C8": "tip_box_64_tipspot_7_2", - "D8": "tip_box_64_tipspot_7_3", - "E8": "tip_box_64_tipspot_7_4", - "F8": "tip_box_64_tipspot_7_5", - "G8": "tip_box_64_tipspot_7_6", - "H8": "tip_box_64_tipspot_7_7" - }, - "num_items_x": 8, - "num_items_y": 8, - "dx": 8.0, - "dy": 8.0, - "item_dx": 9.0, - "item_dy": 9.0 - }, - "data": {} - }, - { - "id": "tip_box_64_tipspot_0_0", - "name": "tip_box_64_tipspot_0_0", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 8.0, - "y": 71.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_0_1", - "name": "tip_box_64_tipspot_0_1", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 8.0, - "y": 62.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_0_2", - "name": "tip_box_64_tipspot_0_2", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 8.0, - "y": 53.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_0_3", - "name": "tip_box_64_tipspot_0_3", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 8.0, - "y": 44.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_0_4", - "name": "tip_box_64_tipspot_0_4", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 8.0, - "y": 35.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_0_5", - "name": "tip_box_64_tipspot_0_5", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 8.0, - "y": 26.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_0_6", - "name": "tip_box_64_tipspot_0_6", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 8.0, - "y": 17.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_0_7", - "name": "tip_box_64_tipspot_0_7", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 8.0, - "y": 8.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_1_0", - "name": "tip_box_64_tipspot_1_0", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 17.0, - "y": 71.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_1_1", - "name": "tip_box_64_tipspot_1_1", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 17.0, - "y": 62.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_1_2", - "name": "tip_box_64_tipspot_1_2", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 17.0, - "y": 53.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_1_3", - "name": "tip_box_64_tipspot_1_3", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 17.0, - "y": 44.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_1_4", - "name": "tip_box_64_tipspot_1_4", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 17.0, - "y": 35.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_1_5", - "name": "tip_box_64_tipspot_1_5", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 17.0, - "y": 26.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_1_6", - "name": "tip_box_64_tipspot_1_6", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 17.0, - "y": 17.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_1_7", - "name": "tip_box_64_tipspot_1_7", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 17.0, - "y": 8.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_2_0", - "name": "tip_box_64_tipspot_2_0", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 26.0, - "y": 71.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_2_1", - "name": "tip_box_64_tipspot_2_1", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 26.0, - "y": 62.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_2_2", - "name": "tip_box_64_tipspot_2_2", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 26.0, - "y": 53.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_2_3", - "name": "tip_box_64_tipspot_2_3", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 26.0, - "y": 44.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_2_4", - "name": "tip_box_64_tipspot_2_4", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 26.0, - "y": 35.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_2_5", - "name": "tip_box_64_tipspot_2_5", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 26.0, - "y": 26.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_2_6", - "name": "tip_box_64_tipspot_2_6", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 26.0, - "y": 17.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_2_7", - "name": "tip_box_64_tipspot_2_7", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 26.0, - "y": 8.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_3_0", - "name": "tip_box_64_tipspot_3_0", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 35.0, - "y": 71.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_3_1", - "name": "tip_box_64_tipspot_3_1", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 35.0, - "y": 62.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_3_2", - "name": "tip_box_64_tipspot_3_2", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 35.0, - "y": 53.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_3_3", - "name": "tip_box_64_tipspot_3_3", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 35.0, - "y": 44.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_3_4", - "name": "tip_box_64_tipspot_3_4", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 35.0, - "y": 35.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_3_5", - "name": "tip_box_64_tipspot_3_5", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 35.0, - "y": 26.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_3_6", - "name": "tip_box_64_tipspot_3_6", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 35.0, - "y": 17.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_3_7", - "name": "tip_box_64_tipspot_3_7", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 35.0, - "y": 8.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_4_0", - "name": "tip_box_64_tipspot_4_0", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 44.0, - "y": 71.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_4_1", - "name": "tip_box_64_tipspot_4_1", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 44.0, - "y": 62.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_4_2", - "name": "tip_box_64_tipspot_4_2", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 44.0, - "y": 53.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_4_3", - "name": "tip_box_64_tipspot_4_3", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 44.0, - "y": 44.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_4_4", - "name": "tip_box_64_tipspot_4_4", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 44.0, - "y": 35.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_4_5", - "name": "tip_box_64_tipspot_4_5", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 44.0, - "y": 26.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_4_6", - "name": "tip_box_64_tipspot_4_6", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 44.0, - "y": 17.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_4_7", - "name": "tip_box_64_tipspot_4_7", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 44.0, - "y": 8.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_5_0", - "name": "tip_box_64_tipspot_5_0", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 53.0, - "y": 71.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_5_1", - "name": "tip_box_64_tipspot_5_1", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 53.0, - "y": 62.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_5_2", - "name": "tip_box_64_tipspot_5_2", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 53.0, - "y": 53.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_5_3", - "name": "tip_box_64_tipspot_5_3", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 53.0, - "y": 44.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_5_4", - "name": "tip_box_64_tipspot_5_4", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 53.0, - "y": 35.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_5_5", - "name": "tip_box_64_tipspot_5_5", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 53.0, - "y": 26.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_5_6", - "name": "tip_box_64_tipspot_5_6", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 53.0, - "y": 17.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_5_7", - "name": "tip_box_64_tipspot_5_7", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 53.0, - "y": 8.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_6_0", - "name": "tip_box_64_tipspot_6_0", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 62.0, - "y": 71.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_6_1", - "name": "tip_box_64_tipspot_6_1", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 62.0, - "y": 62.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_6_2", - "name": "tip_box_64_tipspot_6_2", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 62.0, - "y": 53.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_6_3", - "name": "tip_box_64_tipspot_6_3", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 62.0, - "y": 44.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_6_4", - "name": "tip_box_64_tipspot_6_4", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 62.0, - "y": 35.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_6_5", - "name": "tip_box_64_tipspot_6_5", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 62.0, - "y": 26.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_6_6", - "name": "tip_box_64_tipspot_6_6", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 62.0, - "y": 17.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_6_7", - "name": "tip_box_64_tipspot_6_7", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 62.0, - "y": 8.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_7_0", - "name": "tip_box_64_tipspot_7_0", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 71.0, - "y": 71.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_7_1", - "name": "tip_box_64_tipspot_7_1", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 71.0, - "y": 62.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_7_2", - "name": "tip_box_64_tipspot_7_2", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 71.0, - "y": 53.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_7_3", - "name": "tip_box_64_tipspot_7_3", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 71.0, - "y": 44.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_7_4", - "name": "tip_box_64_tipspot_7_4", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 71.0, - "y": 35.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_7_5", - "name": "tip_box_64_tipspot_7_5", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 71.0, - "y": 26.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_7_6", - "name": "tip_box_64_tipspot_7_6", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 71.0, - "y": 17.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "tip_box_64_tipspot_7_7", - "name": "tip_box_64_tipspot_7_7", - "sample_id": null, - "children": [], - "parent": "tip_box_64", - "type": "container", - "class": "", - "position": { - "x": 71.0, - "y": 8.0, - "z": 0.0 - }, - "config": { - "type": "TipSpot", - "size_x": 10, - "size_y": 10, - "size_z": 0.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "tip_spot", - "model": null, - "barcode": null, - "prototype_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - }, - "data": { - "tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - }, - "tip_state": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - }, - "pending_tip": { - "type": "Tip", - "total_tip_length": 20.0, - "has_filter": false, - "maximal_volume": 1000, - "fitting_depth": 8.0 - } - } - }, - { - "id": "waste_tip_box", - "name": "waste_tip_box", - "sample_id": null, - "children": [], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 300, - "y": 200, - "z": 0 - }, - "config": { - "type": "WasteTipBox", - "size_x": 127.8, - "size_y": 85.5, - "size_z": 60.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "waste_tip_box", - "model": null, - "barcode": null, - "max_volume": "Infinity", - "material_z_thickness": 0, - "compute_volume_from_height": null, - "compute_height_from_volume": null - }, - "data": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - } - } - ], - "links": [] -} \ No newline at end of file diff --git a/unilabos/devices/workstation/workstation_http_service.py b/unilabos/devices/workstation/workstation_http_service.py index 11d87693..82fb6cb5 100644 --- a/unilabos/devices/workstation/workstation_http_service.py +++ b/unilabos/devices/workstation/workstation_http_service.py @@ -459,12 +459,12 @@ def _handle_material_change_report(self, request_data: Dict[str, Any]) -> HttpRe # 验证必需字段 if 'brand' in request_data: if request_data['brand'] == "bioyond": # 奔曜 - error_msg = request_data["text"] - logger.info(f"收到奔曜错误处理报送: {error_msg}") + material_data = request_data["text"] + logger.info(f"收到奔曜物料变更报送: {material_data}") return HttpResponse( success=True, - message=f"错误处理报送已收到: {error_msg}", - acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}", + message=f"物料变更报送已收到: {material_data}", + acknowledgment_id=f"MATERIAL_{int(time.time() * 1000)}_{material_data.get('id', 'unknown')}", data=None ) else: diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml new file mode 100644 index 00000000..9243e21e --- /dev/null +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -0,0 +1,847 @@ +bioyond_cell: + category: + - bioyond_cell + class: + action_value_mappings: + auto-auto_batch_outbound_from_xlsx: + feedback: {} + goal: {} + goal_default: + xlsx_path: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + xlsx_path: + type: string + required: + - xlsx_path + type: object + result: {} + required: + - goal + title: auto_batch_outbound_from_xlsx参数 + type: object + type: UniLabJsonCommand + auto-auto_feeding4to3: + feedback: {} + goal: {} + goal_default: + xlsx_path: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + xlsx_path: + default: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/2025122301.xlsx + type: string + required: [] + type: object + result: {} + required: + - goal + title: auto_feeding4to3参数 + type: object + type: UniLabJsonCommand + auto-create_and_inbound_materials: + feedback: {} + goal: {} + goal_default: + material_names: null + type_id: 3a190ca0-b2f6-9aeb-8067-547e72c11469 + warehouse_name: 粉末加样头堆栈 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + material_names: + type: string + type_id: + default: 3a190ca0-b2f6-9aeb-8067-547e72c11469 + type: string + warehouse_name: + default: 粉末加样头堆栈 + type: string + required: [] + type: object + result: {} + required: + - goal + title: create_and_inbound_materials参数 + type: object + type: UniLabJsonCommand + auto-create_material: + feedback: {} + goal: {} + goal_default: + location_name_or_id: null + material_name: null + type_id: null + warehouse_name: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + location_name_or_id: + type: string + material_name: + type: string + type_id: + type: string + warehouse_name: + type: string + required: + - material_name + - type_id + - warehouse_name + type: object + result: {} + required: + - goal + title: create_material参数 + type: object + type: UniLabJsonCommand + auto-create_materials: + feedback: {} + goal: {} + goal_default: + mappings: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + mappings: + type: object + required: + - mappings + type: object + result: {} + required: + - goal + title: create_materials参数 + type: object + type: UniLabJsonCommand + auto-create_orders: + 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: '' + properties: + feedback: {} + goal: + properties: + xlsx_path: + type: string + required: + - xlsx_path + type: object + result: {} + required: + - goal + 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: {} + goal_default: + board_type: null + bottle_type: null + location_code: null + name: null + warehouse_name: 手动堆栈 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + board_type: + type: string + bottle_type: + type: string + location_code: + type: string + name: + type: string + warehouse_name: + default: 手动堆栈 + type: string + required: + - name + - board_type + - bottle_type + - location_code + type: object + result: {} + required: + - goal + title: create_sample参数 + type: object + type: UniLabJsonCommand + auto-order_list_v2: + feedback: {} + goal: {} + goal_default: + beginTime: '' + endTime: '' + filter: '' + pageCount: 1 + skipCount: 0 + sorting: '' + status: '' + timeType: '' + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + beginTime: + default: '' + type: string + endTime: + default: '' + type: string + filter: + default: '' + type: string + pageCount: + default: 1 + type: integer + skipCount: + default: 0 + type: integer + sorting: + default: '' + type: string + status: + default: '' + type: string + timeType: + default: '' + type: string + required: [] + type: object + result: {} + required: + - goal + title: order_list_v2参数 + type: object + type: UniLabJsonCommand + auto-process_order_finish_report: + feedback: {} + goal: {} + goal_default: + report_request: null + used_materials: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + report_request: + type: string + used_materials: + type: string + required: + - report_request + type: object + result: {} + required: + - goal + title: process_order_finish_report参数 + type: object + type: UniLabJsonCommand + auto-process_sample_finish_report: + feedback: {} + goal: {} + goal_default: + report_request: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + report_request: + type: string + required: + - report_request + type: object + result: {} + required: + - goal + title: process_sample_finish_report参数 + type: object + type: UniLabJsonCommand + auto-process_step_finish_report: + feedback: {} + goal: {} + goal_default: + report_request: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + report_request: + type: string + required: + - report_request + type: object + result: {} + required: + - goal + title: process_step_finish_report参数 + type: object + type: UniLabJsonCommand + auto-report_material_change: + feedback: {} + goal: {} + goal_default: + material_obj: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + material_obj: + type: object + required: + - material_obj + type: object + result: {} + required: + - goal + title: report_material_change参数 + type: object + type: UniLabJsonCommand + auto-resource_tree_transfer: + feedback: {} + goal: {} + goal_default: + old_parent: null + parent_resource: null + plr_resource: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + old_parent: + type: object + parent_resource: + type: object + plr_resource: + type: object + required: + - old_parent + - plr_resource + - parent_resource + type: object + result: {} + required: + - goal + title: resource_tree_transfer参数 + type: object + type: UniLabJsonCommand + auto-scheduler_continue: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: scheduler_continue参数 + type: object + type: UniLabJsonCommand + auto-scheduler_reset: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: scheduler_reset参数 + type: object + type: UniLabJsonCommand + auto-scheduler_start: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: scheduler_start参数 + type: object + type: UniLabJsonCommand + auto-scheduler_start_and_auto_feeding: + feedback: {} + goal: {} + goal_default: + xlsx_path: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + handles: {} + placeholder_keys: {} + result: {} + schema: + description: 组合函数:先启动调度,然后执行自动化上料 + properties: + feedback: {} + goal: + properties: + xlsx_path: + default: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + type: string + required: [] + type: object + result: {} + required: + - goal + title: scheduler_start_and_auto_feeding参数 + type: object + type: UniLabJsonCommand + auto-scheduler_start_and_auto_feeding_v2: + feedback: {} + goal: {} + goal_default: + xlsx_path: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + handles: {} + placeholder_keys: {} + result: {} + schema: + description: 组合函数V2版本(测试):先启动调度,然后执行自动化上料(使用非阻塞轮询等待) + properties: + feedback: {} + goal: + properties: + xlsx_path: + default: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + type: string + required: [] + type: object + result: {} + required: + - goal + title: scheduler_start_and_auto_feeding_v2参数 + type: object + type: UniLabJsonCommand + auto-scheduler_stop: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: scheduler_stop参数 + type: object + type: UniLabJsonCommand + auto-storage_batch_inbound: + feedback: {} + goal: {} + goal_default: + items: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + items: + items: + type: object + type: array + required: + - items + type: object + result: {} + required: + - goal + title: storage_batch_inbound参数 + type: object + type: UniLabJsonCommand + auto-storage_inbound: + feedback: {} + goal: {} + goal_default: + location_id: null + material_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + location_id: + type: string + material_id: + type: string + required: + - material_id + - location_id + type: object + result: {} + required: + - goal + title: storage_inbound参数 + type: object + type: UniLabJsonCommand + auto-transfer_1_to_2: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + 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: {} + 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: '' + properties: + feedback: {} + goal: + properties: + source_wh_id: + default: 3a19debc-84b4-0359-e2d4-b3beea49348b + type: string + source_x: + default: 1 + type: integer + source_y: + default: 1 + type: integer + source_z: + default: 1 + type: integer + required: [] + type: object + result: {} + required: + - goal + title: transfer_3_to_2_to_1参数 + type: object + type: UniLabJsonCommand + auto-update_push_ip: + feedback: {} + goal: {} + goal_default: + ip: null + port: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ip: + type: string + port: + type: string + required: [] + type: object + result: {} + required: + - goal + title: update_push_ip参数 + type: object + type: UniLabJsonCommand + auto-wait_for_order_finish: + feedback: {} + goal: {} + goal_default: + order_code: null + timeout: 36000 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + order_code: + type: string + timeout: + default: 36000 + type: integer + required: + - order_code + type: object + result: {} + required: + - goal + title: wait_for_order_finish参数 + type: object + type: UniLabJsonCommand + auto-wait_for_transfer_task: + feedback: {} + goal: {} + goal_default: + filter_text: null + interval: 5 + timeout: 3000 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + filter_text: + type: string + interval: + default: 5 + type: integer + timeout: + default: 3000 + type: integer + required: [] + type: object + result: {} + required: + - goal + title: wait_for_transfer_task参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation + status_types: + device_id: String + type: python + config_info: [] + description: '' + handles: [] + icon: benyao2.webp + init_param_schema: + config: + properties: + config: + type: object + deck: + type: string + protocol_type: + type: string + required: [] + type: object + data: + properties: + device_id: + type: string + required: + - device_id + type: object + registry_type: device + version: 1.0.0 diff --git a/unilabos/registry/devices/bioyond_dispensing_station.yaml b/unilabos/registry/devices/bioyond_dispensing_station.yaml index 9ae76b7f..97b55cc1 100644 --- a/unilabos/registry/devices/bioyond_dispensing_station.yaml +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -5,229 +5,6 @@ bioyond_dispensing_station: - bioyond_dispensing_station class: action_value_mappings: - auto-brief_step_parameters: - feedback: {} - goal: {} - goal_default: - data: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - data: - type: object - required: - - data - type: object - result: {} - required: - - goal - title: brief_step_parameters参数 - type: object - type: UniLabJsonCommand - auto-compute_experiment_design: - feedback: {} - goal: {} - goal_default: - m_tot: '70' - ratio: null - titration_percent: '0.03' - wt_percent: '0.25' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - m_tot: - default: '70' - type: string - ratio: - type: object - titration_percent: - default: '0.03' - type: string - wt_percent: - default: '0.25' - type: string - required: - - ratio - type: object - result: - properties: - feeding_order: - items: {} - title: Feeding Order - type: array - return_info: - title: Return Info - type: string - solutions: - items: {} - title: Solutions - type: array - solvents: - additionalProperties: true - title: Solvents - type: object - titration: - additionalProperties: true - title: Titration - type: object - required: - - solutions - - titration - - solvents - - feeding_order - - return_info - title: ComputeExperimentDesignReturn - type: object - required: - - goal - title: compute_experiment_design参数 - type: object - type: UniLabJsonCommand - auto-process_order_finish_report: - feedback: {} - goal: {} - goal_default: - report_request: null - used_materials: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - report_request: - type: string - used_materials: - type: string - required: - - report_request - - used_materials - type: object - result: {} - required: - - goal - title: process_order_finish_report参数 - type: object - type: UniLabJsonCommand - auto-project_order_report: - feedback: {} - goal: {} - goal_default: - order_id: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - order_id: - type: string - required: - - order_id - type: object - result: {} - required: - - goal - title: project_order_report参数 - type: object - type: UniLabJsonCommand - auto-query_resource_by_name: - feedback: {} - goal: {} - goal_default: - material_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_name: - type: string - required: - - material_name - type: object - result: {} - required: - - goal - title: query_resource_by_name参数 - type: object - type: UniLabJsonCommand - auto-transfer_materials_to_reaction_station: - feedback: {} - goal: {} - goal_default: - target_device_id: null - transfer_groups: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - target_device_id: - type: string - transfer_groups: - type: array - required: - - target_device_id - - transfer_groups - type: object - result: {} - required: - - goal - title: transfer_materials_to_reaction_station参数 - type: object - type: UniLabJsonCommand - auto-workflow_sample_locations: - feedback: {} - goal: {} - goal_default: - workflow_id: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - workflow_id: - type: string - required: - - workflow_id - type: object - result: {} - required: - - goal - title: workflow_sample_locations参数 - type: object - type: UniLabJsonCommand batch_create_90_10_vial_feeding_tasks: feedback: {} goal: @@ -394,6 +171,99 @@ bioyond_dispensing_station: title: BatchCreateDiamineSolutionTasks type: object type: UniLabJsonCommand + compute_experiment_design: + feedback: {} + goal: + m_tot: m_tot + ratio: ratio + titration_percent: titration_percent + wt_percent: wt_percent + goal_default: + m_tot: '70' + ratio: '' + titration_percent: '0.03' + wt_percent: '0.25' + handles: + output: + - data_key: solutions + data_source: executor + data_type: array + handler_key: solutions + io_type: sink + label: Solution Data From Python + - data_key: titration + data_source: executor + data_type: object + handler_key: titration + io_type: sink + label: Titration Data From Calculation Node + - data_key: solvents + data_source: executor + data_type: object + handler_key: solvents + io_type: sink + label: Solvents Data From Calculation Node + - data_key: feeding_order + data_source: executor + data_type: array + handler_key: feeding_order + io_type: sink + label: Feeding Order Data From Calculation Node + result: + feeding_order: feeding_order + return_info: return_info + solutions: solutions + solvents: solvents + titration: titration + schema: + description: 计算实验设计,输出solutions/titration/solvents/feeding_order用于后续节点。 + properties: + feedback: {} + goal: + properties: + m_tot: + default: '70' + description: 总质量(g) + type: string + ratio: + description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1} + type: string + titration_percent: + default: '0.03' + description: 滴定比例(10%部分) + type: string + wt_percent: + default: '0.25' + description: 目标固含质量分数 + type: string + required: + - ratio + type: object + result: + properties: + feeding_order: + type: array + return_info: + type: string + solutions: + type: array + solvents: + type: object + titration: + type: object + required: + - solutions + - titration + - solvents + - feeding_order + - return_info + title: ComputeExperimentDesign_Result + type: object + required: + - goal + title: ComputeExperimentDesign + type: object + type: UniLabJsonCommand create_90_10_vial_feeding_task: feedback: {} goal: @@ -620,6 +490,89 @@ bioyond_dispensing_station: title: DispenStationSolnPrep type: object type: DispenStationSolnPrep + scheduler_start: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: + return_info: return_info + schema: + description: 启动调度器 - 启动Bioyond配液站的任务调度器,开始执行队列中的任务 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + return_info: + description: 调度器启动结果,成功返回1,失败返回0 + type: integer + required: + - return_info + title: scheduler_start结果 + type: object + required: + - goal + title: scheduler_start参数 + type: object + type: UniLabJsonCommand + transfer_materials_to_reaction_station: + feedback: {} + goal: + target_device_id: target_device_id + transfer_groups: transfer_groups + goal_default: + target_device_id: '' + transfer_groups: '' + handles: {} + placeholder_keys: + target_device_id: unilabos_devices + result: {} + schema: + description: 将配液站完成的物料(溶液、样品等)转移到指定反应站的堆栈库位。支持配置多组转移任务,每组包含物料名称、目标堆栈和目标库位。 + properties: + feedback: {} + goal: + properties: + target_device_id: + description: 目标反应站设备ID(从设备列表中选择,所有转移组都使用同一个目标设备) + type: string + transfer_groups: + description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组 + items: + properties: + materials: + description: 物料名称(手动输入,系统将通过RPC查询验证) + type: string + target_sites: + description: 目标库位(手动输入,如"A01") + type: string + target_stack: + description: 目标堆栈名称(从列表选择) + enum: + - 堆栈1左 + - 堆栈1右 + - 站内试剂存放堆栈 + type: string + required: + - materials + - target_stack + - target_sites + type: object + type: array + required: + - target_device_id + - transfer_groups + type: object + result: {} + required: + - goal + title: transfer_materials_to_reaction_station参数 + type: object + type: UniLabJsonCommand wait_for_multiple_orders_and_get_reports: feedback: {} goal: @@ -688,7 +641,7 @@ bioyond_dispensing_station: title: WaitForMultipleOrdersAndGetReports type: object type: UniLabJsonCommand - module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation + module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation status_types: {} type: python config_info: [] @@ -699,15 +652,16 @@ bioyond_dispensing_station: config: properties: config: - type: string + type: object deck: type: string - required: - - config - - deck + protocol_type: + type: string + required: [] type: object data: properties: {} required: [] type: object + model: {} version: 1.0.0 diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml new file mode 100644 index 00000000..c8a671a7 --- /dev/null +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -0,0 +1,850 @@ +coincellassemblyworkstation_device: + category: + - coin_cell_workstation + class: + action_value_mappings: + auto-change_hole_sheet_to_2: + feedback: {} + goal: {} + goal_default: + hole: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + hole: + type: object + required: + - hole + type: object + result: {} + required: + - goal + title: change_hole_sheet_to_2参数 + type: object + type: UniLabJsonCommandAsync + auto-fill_plate: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: fill_plate参数 + type: object + type: UniLabJsonCommandAsync + auto-fun_wuliao_test: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: fun_wuliao_test参数 + type: object + type: UniLabJsonCommand + auto-func_allpack_cmd: + feedback: {} + goal: {} + goal_default: + assembly_pressure: 4200 + assembly_type: 7 + elec_num: null + elec_use_num: null + elec_vol: 50 + file_path: /Users/sml/work + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + assembly_pressure: + default: 4200 + type: integer + assembly_type: + default: 7 + type: integer + elec_num: + type: string + elec_use_num: + type: string + elec_vol: + default: 50 + type: integer + file_path: + default: /Users/sml/work + type: string + required: + - elec_num + - elec_use_num + type: object + result: {} + required: + - goal + title: func_allpack_cmd参数 + type: object + type: UniLabJsonCommand + auto-func_allpack_cmd_simp: + 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: {} + 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: 电解液瓶数 + 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_allpack_cmd_simp参数 + type: object + type: UniLabJsonCommand + auto-func_get_csv_export_status: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_get_csv_export_status参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_auto: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_auto参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_init: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_init参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_init_auto_start_combined: + feedback: {} + goal: {} + goal_default: + material_search_enable: false + handles: {} + placeholder_keys: {} + result: {} + schema: + description: 组合函数:设备初始化 + 物料搜寻确认 + 切换自动模式 + 启动。初始化过程中会自动检测物料搜寻确认弹窗,并根据参数自动点击"是"或"否"按钮 + properties: + feedback: {} + goal: + properties: + material_search_enable: + default: false + description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻) + type: boolean + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_init_auto_start_combined参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_start: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_start参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_stop: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_stop参数 + type: object + type: UniLabJsonCommand + auto-func_pack_get_msg_cmd: + feedback: {} + goal: {} + goal_default: + file_path: D:\coin_cell_data + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + file_path: + default: D:\coin_cell_data + type: string + required: [] + type: object + result: {} + required: + - goal + title: func_pack_get_msg_cmd参数 + type: object + type: UniLabJsonCommand + auto-func_pack_send_bottle_num: + feedback: {} + goal: {} + goal_default: + bottle_num: null + handles: + input: + - data_key: bottle_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: + bottle_num: + type: integer + required: + - bottle_num + type: object + result: {} + required: + - goal + title: func_pack_send_bottle_num参数 + type: object + type: UniLabJsonCommand + auto-func_pack_send_finished_cmd: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_send_finished_cmd参数 + type: object + type: UniLabJsonCommand + auto-func_pack_send_msg_cmd: + feedback: {} + goal: {} + goal_default: + assembly_pressure: null + assembly_type: null + elec_use_num: null + elec_vol: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + assembly_pressure: + type: string + assembly_type: + type: string + elec_use_num: + type: string + elec_vol: + type: string + required: + - elec_use_num + - elec_vol + - assembly_type + - assembly_pressure + type: object + result: {} + required: + - goal + title: func_pack_send_msg_cmd参数 + type: object + type: UniLabJsonCommand + auto-func_read_data_and_output: + feedback: {} + goal: {} + goal_default: + file_path: /Users/sml/work + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + file_path: + default: /Users/sml/work + type: string + required: [] + type: object + result: {} + required: + - goal + 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: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_stop_read_data参数 + type: object + type: UniLabJsonCommand + auto-modify_deck_name: + feedback: {} + goal: {} + goal_default: + resource_name: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + resource_name: + type: string + required: + - resource_name + type: object + result: {} + required: + - goal + title: modify_deck_name参数 + 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-qiming_coin_cell_code: + feedback: {} + goal: {} + goal_default: + battery_clean_ignore: false + battery_pressure: 4000 + battery_pressure_mode: true + fujipian_juzhendianwei: 0 + fujipian_panshu: null + gemo_juzhendianwei: 0 + gemopanshu: 0 + lvbodian: true + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + battery_clean_ignore: + default: false + type: boolean + battery_pressure: + default: 4000 + type: integer + battery_pressure_mode: + default: true + type: boolean + fujipian_juzhendianwei: + default: 0 + type: integer + fujipian_panshu: + type: integer + gemo_juzhendianwei: + default: 0 + type: integer + gemopanshu: + default: 0 + type: integer + lvbodian: + default: true + type: boolean + required: + - fujipian_panshu + type: object + result: {} + required: + - goal + title: qiming_coin_cell_code参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation + status_types: + data_assembly_coin_cell_num: int + data_assembly_pressure: int + data_assembly_time: float + data_axis_x_pos: float + data_axis_y_pos: float + data_axis_z_pos: float + data_coin_cell_code: str + data_coin_num: int + data_electrolyte_code: str + data_electrolyte_volume: int + data_glove_box_o2_content: float + data_glove_box_pressure: float + data_glove_box_water_content: float + data_open_circuit_voltage: float + data_pole_weight: float + request_rec_msg_status: bool + request_send_msg_status: bool + sys_mode: str + sys_status: str + type: python + config_info: [] + description: '' + handles: [] + icon: koudian.webp + init_param_schema: + config: + properties: + address: + default: 172.16.28.102 + type: string + config: + type: object + debug_mode: + default: false + type: boolean + deck: + type: string + port: + default: '502' + type: string + required: [] + type: object + data: + properties: + data_assembly_coin_cell_num: + type: integer + data_assembly_pressure: + type: integer + data_assembly_time: + type: number + data_axis_x_pos: + type: number + data_axis_y_pos: + type: number + data_axis_z_pos: + type: number + data_coin_cell_code: + type: string + data_coin_num: + type: integer + data_electrolyte_code: + type: string + data_electrolyte_volume: + type: integer + data_glove_box_o2_content: + type: number + data_glove_box_pressure: + type: number + data_glove_box_water_content: + type: number + data_open_circuit_voltage: + type: number + data_pole_weight: + type: number + request_rec_msg_status: + type: boolean + request_send_msg_status: + type: boolean + sys_mode: + type: string + sys_status: + type: string + required: + - sys_status + - sys_mode + - request_rec_msg_status + - request_send_msg_status + - data_assembly_coin_cell_num + - data_assembly_time + - data_open_circuit_voltage + - data_axis_x_pos + - data_axis_y_pos + - data_axis_z_pos + - data_pole_weight + - data_assembly_pressure + - data_electrolyte_volume + - data_coin_num + - data_coin_cell_code + - data_electrolyte_code + - data_glove_box_pressure + - data_glove_box_o2_content + - data_glove_box_water_content + type: object + registry_type: device + version: 1.0.0 diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index b7d10a60..cf46d7ef 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -4,213 +4,88 @@ reaction_station.bioyond: - reaction_station_bioyond class: action_value_mappings: - auto-create_order: + add_time_constraint: feedback: {} - goal: {} - goal_default: - json_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - json_str: - type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: create_order参数 - type: object - type: UniLabJsonCommand - auto-hard_delete_merged_workflows: - feedback: {} - goal: {} - goal_default: - workflow_ids: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - workflow_ids: - items: - type: string - type: array - required: - - workflow_ids - type: object - result: {} - required: - - goal - title: hard_delete_merged_workflows参数 - type: object - type: UniLabJsonCommand - auto-merge_workflow_with_parameters: - feedback: {} - goal: {} - goal_default: - json_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - json_str: - type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: merge_workflow_with_parameters参数 - type: object - type: UniLabJsonCommand - auto-process_temperature_cutoff_report: - feedback: {} - goal: {} - goal_default: - report_request: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - report_request: - type: string - required: - - report_request - type: object - result: {} - required: - - goal - title: process_temperature_cutoff_report参数 - type: object - type: UniLabJsonCommand - auto-process_web_workflows: - feedback: {} - goal: {} - goal_default: - web_workflow_json: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - web_workflow_json: - type: string - required: - - web_workflow_json - type: object - result: {} - required: - - goal - title: process_web_workflows参数 - type: object - type: UniLabJsonCommand - auto-skip_titration_steps: - feedback: {} - goal: {} + goal: + duration: duration + end_point: end_point + end_step_key: end_step_key + start_point: start_point + start_step_key: start_step_key goal_default: - preintake_id: null + duration: 0 + end_point: 0 + end_step_key: '' + start_point: 0 + start_step_key: '' handles: {} - placeholder_keys: {} result: {} schema: - description: '' + description: 添加时间约束 - 在两个工作流之间添加时间约束 properties: feedback: {} goal: properties: - preintake_id: + duration: + description: 时间(秒) + type: integer + end_point: + default: Start + description: 终点计时点 (Start=开始前, End=结束后) + enum: + - Start + - End + type: string + end_step_key: + description: 终点步骤Key (可选, 默认为空则自动选择) + type: string + start_point: + default: Start + description: 起点计时点 (Start=开始前, End=结束后) + enum: + - Start + - End + type: string + start_step_key: + description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择) type: string required: - - preintake_id + - duration type: object result: {} required: - goal - title: skip_titration_steps参数 + title: add_time_constraint参数 type: object type: UniLabJsonCommand - auto-wait_for_multiple_orders_and_get_reports: + clean_all_server_workflows: feedback: {} goal: {} - goal_default: - batch_create_result: null - check_interval: 10 - timeout: 7200 + goal_default: {} handles: {} - placeholder_keys: {} - result: {} + result: + code: code + message: message schema: - description: '' + description: 清空服务端所有非核心工作流 (保留核心流程) properties: feedback: {} goal: - properties: - batch_create_result: - type: string - check_interval: - default: 10 - type: integer - timeout: - default: 7200 - type: integer + properties: {} required: [] type: object - result: {} - required: - - goal - title: wait_for_multiple_orders_and_get_reports参数 - type: object - type: UniLabJsonCommand - auto-workflow_step_query: - feedback: {} - goal: {} - goal_default: - workflow_id: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: + result: properties: - workflow_id: + code: + description: 操作结果代码(1表示成功) + type: integer + message: + description: 结果描述 type: string - required: - - workflow_id type: object - result: {} required: - goal - title: workflow_step_query参数 + title: clean_all_server_workflows参数 type: object type: UniLabJsonCommand drip_back: @@ -247,13 +122,19 @@ reaction_station.bioyond: description: 观察时间(分钟) type: string titration_type: - description: 是否滴定(1=否, 2=是) + description: 是否滴定(NO=否, YES=是) + enum: + - 'NO' + - 'YES' type: string torque_variation: - description: 是否观察 (1=否, 2=是) + description: 是否观察 (NO=否, YES=是) + enum: + - 'NO' + - 'YES' type: string volume: - description: 分液公式(μL) + description: 分液公式(mL) type: string required: - volume @@ -353,13 +234,19 @@ reaction_station.bioyond: description: 观察时间(分钟) type: string titration_type: - description: 是否滴定(1=否, 2=是) + description: 是否滴定(NO=否, YES=是) + enum: + - 'NO' + - 'YES' type: string torque_variation: - description: 是否观察 (1=否, 2=是) + description: 是否观察 (NO=否, YES=是) + enum: + - 'NO' + - 'YES' type: string volume: - description: 分液公式(μL) + description: 分液公式(mL) type: string required: - volume @@ -403,7 +290,7 @@ reaction_station.bioyond: label: Solvents Data From Calculation Node result: {} schema: - description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。 + description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。 properties: feedback: {} goal: @@ -423,15 +310,21 @@ reaction_station.bioyond: description: 观察时间(分钟),默认360 type: string titration_type: - default: '1' - description: 是否滴定(1=否, 2=是),默认1 + default: 'NO' + description: 是否滴定(NO=否, YES=是),默认NO + enum: + - 'NO' + - 'YES' type: string torque_variation: - default: '2' - description: 是否观察 (1=否, 2=是),默认2 + default: 'YES' + description: 是否观察 (NO=否, YES=是),默认YES + enum: + - 'NO' + - 'YES' type: string volume: - description: 分液量(μL)。可直接提供,或通过solvents参数自动计算 + description: 分液量(mL)。可直接提供,或通过solvents参数自动计算 type: string required: - assign_material_name @@ -504,15 +397,21 @@ reaction_station.bioyond: description: 观察时间(分钟),默认90 type: string titration_type: - default: '2' - description: 是否滴定(1=否, 2=是),默认2 + default: 'YES' + description: 是否滴定(NO=否, YES=是),默认YES + enum: + - 'NO' + - 'YES' type: string torque_variation: - default: '2' - description: 是否观察 (1=否, 2=是),默认2 + default: 'YES' + description: 是否观察 (NO=否, YES=是),默认YES + enum: + - 'NO' + - 'YES' type: string volume_formula: - description: 分液公式(μL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成 + description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成 type: string x_value: description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算 @@ -560,13 +459,19 @@ reaction_station.bioyond: description: 观察时间(分钟) type: string titration_type: - description: 是否滴定(1=否, 2=是) + description: 是否滴定(NO=否, YES=是) + enum: + - 'NO' + - 'YES' type: string torque_variation: - description: 是否观察 (1=否, 2=是) + description: 是否观察 (NO=否, YES=是) + enum: + - 'NO' + - 'YES' type: string volume_formula: - description: 分液公式(μL) + description: 分液公式(mL) type: string required: - volume_formula @@ -680,6 +585,35 @@ reaction_station.bioyond: title: reactor_taken_out参数 type: object type: UniLabJsonCommand + scheduler_start: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: + return_info: return_info + schema: + description: 启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + return_info: + description: 调度器启动结果,成功返回1,失败返回0 + type: integer + required: + - return_info + title: scheduler_start结果 + type: object + required: + - goal + title: scheduler_start参数 + type: object + type: UniLabJsonCommand solid_feeding_vials: feedback: {} goal: @@ -706,7 +640,11 @@ reaction_station.bioyond: description: 物料名称(用于获取试剂瓶位ID) type: string material_id: - description: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟) + description: 粉末类型ID,Salt=盐(21分钟),Flour=面粉(27分钟),BTDA=BTDA(38分钟) + enum: + - Salt + - Flour + - BTDA type: string temperature: description: 温度设定(°C) @@ -715,7 +653,10 @@ reaction_station.bioyond: description: 观察时间(分钟) type: string torque_variation: - description: 是否观察 (1=否, 2=是) + description: 是否观察 (NO=否, YES=是) + enum: + - 'NO' + - 'YES' type: string required: - assign_material_name @@ -730,9 +671,19 @@ reaction_station.bioyond: title: solid_feeding_vials参数 type: object type: UniLabJsonCommand - module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation + module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactionStation protocol_type: [] status_types: + average_viscosity: Float64 + force: Float64 + in_temperature: Float64 + out_temperature: Float64 + pt100_temperature: Float64 + sensor_average_temperature: Float64 + setting_temperature: Float64 + speed: Float64 + target_temperature: Float64 + viscosity: Float64 workflow_sequence: String type: python config_info: [] @@ -765,34 +716,19 @@ reaction_station.reactor: - reactor - reaction_station_bioyond class: - action_value_mappings: - auto-update_metrics: - feedback: {} - goal: {} - goal_default: - payload: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - payload: - type: object - required: - - payload - type: object - result: {} - required: - - goal - title: update_metrics参数 - type: object - type: UniLabJsonCommand - module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactor - status_types: {} + action_value_mappings: {} + module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactor + status_types: + average_viscosity: Float64 + force: Float64 + in_temperature: Float64 + out_temperature: Float64 + pt100_temperature: Float64 + sensor_average_temperature: Float64 + setting_temperature: Float64 + speed: Float64 + target_temperature: Float64 + viscosity: Float64 type: python config_info: [] description: 反应站子设备-反应器 diff --git a/unilabos/registry/resources/bioyond/README_RESOURCE_ARCHITECTURE.md b/unilabos/registry/resources/bioyond/README_RESOURCE_ARCHITECTURE.md new file mode 100644 index 00000000..cc84ef78 --- /dev/null +++ b/unilabos/registry/resources/bioyond/README_RESOURCE_ARCHITECTURE.md @@ -0,0 +1,170 @@ +# UniLabOS 资源注册架构详解 + +> **目标受众**: 主要开发 `unilabos/registry/devices` 抽象层的开发者 +> **最后更新**: 2026-01-11 +> **维护者**: Uni-Lab-OS 开发团队 + +本文档详细说明 UniLabOS 资源注册系统的架构、资源的完整生命周期,以及如何实现动态物料位置追踪。 + +--- + +## 📚 目录 + +- [核心概念](#核心概念) +- [三层架构详解](#三层架构详解) +- [资源注册机制](#资源注册机制) +- [物料生命周期管理](#物料生命周期管理) +- [动态物料位置追踪](#动态物料位置追踪) +- [实战案例](#实战案例) +- [常见问题排查](#常见问题排查) + +--- + +## 核心概念 + +### 1. Resources vs Registry + +UniLabOS 采用**声明式注册**模式,将资源的**定义**(Python)与**注册信息**(YAML)分离: + +``` +┌──────────────────────────────────────────────────────────┐ +│ unilabos/resources (Python 实现) │ +│ - 定义资源的物理属性、行为和创建逻辑 │ +│ - 例如: 瓶子的尺寸、容量、材质 │ +├──────────────────────────────────────────────────────────┤ +│ unilabos/registry/resources (YAML 注册表) │ +│ - 声明哪些资源可以被前端使用 │ +│ - 定义资源的分类、图标、初始化参数 │ +└──────────────────────────────────────────────────────────┘ +``` + +**为什么要分离?** + +1. **解耦**: Python 代码可以定义无限多的资源类,但只有在 YAML 中注册的才能被前端识别 +2. **灵活性**: 无需修改 Python 代码,只需修改 YAML 就能添加/移除可用资源 +3. **可扩展性**: 第三方开发者可以通过 YAML 注册自己的资源,无需修改核心代码 + +--- + +## 三层架构详解 + +UniLabOS 资源系统采用**三层架构**,实现从前端UI到底层硬件的完整映射: + +### 架构图 + +``` +┌─────────────────────────────────────────────────────┐ +│ 第1层: YAML 注册表 (registry/resources) │ +│ - 告诉系统"哪些资源可用" │ +│ - 前端通过此层获取可用资源列表 │ +│ - 文件: YB_bottle.yaml, YB_bottle_carriers.yaml │ +├─────────────────────────────────────────────────────┤ +│ 第2层: Python 实现 (resources/bioyond) │ +│ - 定义资源的具体属性和行为 │ +│ - 创建资源实例的工厂函数 │ +│ - 文件: YB_bottles.py, YB_bottle_carriers.py │ +├─────────────────────────────────────────────────────┤ +│ 第3层: Hardware/API 集成 (devices/workstation) │ +│ - 连接 Bioyond 系统 API │ +│ - 同步物料位置和状态 │ +│ - 文件: station.py, bioyond_rpc.py, config.py │ +└─────────────────────────────────────────────────────┘ +``` + +### 第1层: YAML 注册表 + +#### YB_bottle.yaml - 单个瓶子注册 + +```yaml +YB_5ml_fenyeping: + category: + - yb3 # 系统分类 + - YB_bottle # 资源类型 + class: + module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping # Python 函数路径 + type: pylabrobot # 框架类型 + description: YB_5ml_fenyeping # 前端显示名称 + handles: [] + icon: '' # 图标路径 + init_param_schema: {} # 初始化参数 schema + registry_type: resource + version: 1.0.0 +``` + +**作用:** +- 前端通过读取此文件知道有一个叫 "YB_5ml_fenyeping" 的资源 +- 用户拖拽时,系统会调用 `YB_bottles:YB_5ml_fenyeping()` 创建实例 + +#### YB_bottle_carriers.yaml - 载架(容器)注册 + +```yaml +YB_5ml_fenyepingban: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban + type: pylabrobot + description: YB_5ml_fenyepingban # 5ml分液瓶板 +``` + +**作用:** +- 载架是容器,里面可以放多个瓶子 +- 例如: `YB_5ml_fenyepingban` 是一个 4x2 布局的板,可以放 8 个 5ml 瓶子 + +### 第2层: Python 实现 + +#### YB_bottles.py - 瓶子工厂函数 + +```python +def YB_5ml_fenyeping( + name: str, + diameter: float = 20.0, # 直径 (mm) + height: float = 50.0, # 高度 (mm) + max_volume: float = 5000.0, # 最大容量 (μL) + barcode: str = None, +) -> Bottle: + \"\"\"创建5ml分液瓶\"\"\" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_5ml_fenyeping", # ⭐ 与 YAML 中的名称对应 + ) +``` + +**关键点:** +- 函数名 `YB_5ml_fenyeping` 必须与 YAML 中的 `module` 路径末尾一致 +- 返回一个 `Bottle` 对象(PyLabRobot 资源类型) +- `model` 字段用于在 Bioyond 系统中识别资源类型 + +**详细文档请参考完整版 README** + +--- + +## 相关文件索引 + +### 核心文件 + +| 文件 | 功能 | 路径 | +|------|------|------| +| `YB_bottle.yaml` | 瓶子注册表 | `unilabos/registry/resources/bioyond/` | +| `YB_bottle_carriers.yaml` | 载架注册表 | `unilabos/registry/resources/bioyond/` | +| `deck.yaml` | Deck注册表 | `unilabos/registry/resources/bioyond/` | +| `YB_bottles.py` | 瓶子实现 | `unilabos/resources/bioyond/` | +| `YB_bottle_carriers.py` | 载架实现 | `unilabos/resources/bioyond/` | +| `YB_warehouses.py` | 仓库实现 | `unilabos/resources/bioyond/` | +| `decks.py` | Deck布局 | `unilabos/resources/bioyond/` | +| `station.py` | 物料同步 | `unilabos/devices/workstation/bioyond_studio/` | +| `config.py` | UUID映射 | `unilabos/devices/workstation/bioyond_studio/` | + +### 仓库相关文档 + +- [README_WAREHOUSE.md](../../resources/bioyond/README_WAREHOUSE.md) - 仓库系统开发指南 + +--- + +**维护者:** Uni-Lab-OS 开发团队 +**最后更新:** 2026-01-11 diff --git a/unilabos/registry/resources/bioyond/YB_bottle.yaml b/unilabos/registry/resources/bioyond/YB_bottle.yaml new file mode 100644 index 00000000..f8e17261 --- /dev/null +++ b/unilabos/registry/resources/bioyond/YB_bottle.yaml @@ -0,0 +1,92 @@ +YB_20ml_fenyeping: + category: + - yb3 + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping + type: pylabrobot + description: YB_20ml_fenyeping + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_5ml_fenyeping: + category: + - yb3 + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping + type: pylabrobot + description: YB_5ml_fenyeping + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_jia_yang_tou_da: + category: + - yb3 + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da + type: pylabrobot + description: YB_jia_yang_tou_da + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_pei_ye_da_Bottle: + category: + - yb3 + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle + type: pylabrobot + description: YB_pei_ye_da_Bottle + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_pei_ye_xiao_Bottle: + category: + - yb3 + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle + type: pylabrobot + description: YB_pei_ye_xiao_Bottle + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_qiang_tou: + category: + - yb3 + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou + type: pylabrobot + description: YB_qiang_tou + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_ye_Bottle: + category: + - yb3 + - YB_bottle_carriers + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle + type: pylabrobot + description: YB_ye_Bottle + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml new file mode 100644 index 00000000..4698a266 --- /dev/null +++ b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml @@ -0,0 +1,182 @@ +YB_100ml_yeti: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti + type: pylabrobot + description: YB_100ml_yeti + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_20ml_fenyepingban: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban + type: pylabrobot + description: YB_20ml_fenyepingban + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_5ml_fenyepingban: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban + type: pylabrobot + description: YB_5ml_fenyepingban + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_6StockCarrier: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier + type: pylabrobot + description: YB_6StockCarrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_6VialCarrier: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier + type: pylabrobot + description: YB_6VialCarrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_gao_nian_ye_Bottle: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle + type: pylabrobot + description: YB_gao_nian_ye_Bottle + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_gaonianye: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye + type: pylabrobot + description: YB_gaonianye + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_jia_yang_tou_da_Carrier: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier + type: pylabrobot + description: YB_jia_yang_tou_da_Carrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_peiyepingdaban: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban + type: pylabrobot + description: YB_peiyepingdaban + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_peiyepingxiaoban: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban + type: pylabrobot + description: YB_peiyepingxiaoban + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_qiang_tou_he: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he + type: pylabrobot + description: YB_qiang_tou_he + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_shi_pei_qi_kuai: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai + type: pylabrobot + description: YB_shi_pei_qi_kuai + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_ye: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye + type: pylabrobot + description: YB_ye_Bottle_Carrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_ye_100ml_Bottle: + category: + - yb3 + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle + type: pylabrobot + description: YB_ye_100ml_Bottle + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/bottles.yaml b/unilabos/registry/resources/bioyond/bottles.yaml index 79aa712b..ecc5525d 100644 --- a/unilabos/registry/resources/bioyond/bottles.yaml +++ b/unilabos/registry/resources/bioyond/bottles.yaml @@ -20,6 +20,17 @@ BIOYOND_PolymerStation_Liquid_Vial: icon: '' init_param_schema: {} version: 1.0.0 +BIOYOND_PolymerStation_Measurement_Vial: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Measurement_Vial + type: pylabrobot + description: 聚合站-测量小瓶(测密度) + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 BIOYOND_PolymerStation_Reactor: category: - bottles diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index bc158508..8d6993b1 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -22,15 +22,27 @@ BIOYOND_PolymerReactionStation_Deck: init_param_schema: {} registry_type: resource version: 1.0.0 -YB_Deck11: +BIOYOND_YB_Deck: category: - deck class: module: unilabos.resources.bioyond.decks:YB_Deck type: pylabrobot - description: BIOYOND PolymerReactionStation Deck + description: BIOYOND ElectrolyteFormulationStation Deck handles: [] icon: 配液站.webp init_param_schema: {} registry_type: resource version: 1.0.0 +CoincellDeck: + category: + - deck + class: + module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:YH_Deck + type: pylabrobot + description: YIHUA CoinCellAssembly Deck + handles: [] + icon: koudian.webp + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/resources/battery/__init__.py b/unilabos/resources/battery/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/resources/battery/bottle_carriers.py b/unilabos/resources/battery/bottle_carriers.py new file mode 100644 index 00000000..9d9827cd --- /dev/null +++ b/unilabos/resources/battery/bottle_carriers.py @@ -0,0 +1,56 @@ +from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d + +from unilabos.resources.itemized_carrier import Bottle, BottleCarrier +from unilabos.resources.bioyond.YB_bottles import ( + YB_pei_ye_xiao_Bottle, +) +# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial + + +def YIHUA_Electrolyte_12VialCarrier(name: str) -> BottleCarrier: + """12瓶载架 - 2x6布局""" + # 载架尺寸 (mm) + carrier_size_x = 120.0 + carrier_size_y = 250.0 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 35.0 + bottle_spacing_x = 35.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (6 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=2, + num_items_y=6, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="Electrolyte_12VialCarrier", + ) + carrier.num_items_x = 2 + carrier.num_items_y = 6 + carrier.num_items_z = 1 + for i in range(12): + carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}") + return carrier diff --git a/unilabos/resources/battery/electrode_sheet.py b/unilabos/resources/battery/electrode_sheet.py new file mode 100644 index 00000000..22f98aff --- /dev/null +++ b/unilabos/resources/battery/electrode_sheet.py @@ -0,0 +1,195 @@ +from typing import Any, Dict, Optional, TypedDict + +from pylabrobot.resources import Resource as ResourcePLR +from pylabrobot.resources import Container + + +electrode_colors = { + "PositiveCan": "#ff0000", + "PositiveElectrode": "#cc3333", + "NegativeCan": "#000000", + "NegativeElectrode": "#666666", + "SpringWasher": "#8b7355", + "FlatWasher": "a9a9a9", + "AluminumFoil": "#ffcccc", + "Battery": "#00ff00", +} + +class ElectrodeSheetState(TypedDict): + diameter: float # 直径 (mm) + thickness: float # 厚度 (mm) + mass: float # 质量 (g) + material_type: str # 材料类型(铜、铝、不锈钢、弹簧钢等) + color: str # 材料类型对应的颜色 + info: Optional[str] # 附加信息 + + +class ElectrodeSheet(ResourcePLR): + """极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料""" + + def __init__( + self, + name: str = "极片", + size_x: float = 10, + size_y: float = 10, + size_z: float = 10, + category: str = "electrode_sheet", + model: Optional[str] = None, + **kwargs + ): + """初始化极片 + + Args: + name: 极片名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + category: 类别 + model: 型号 + **kwargs: 其他参数传递给父类 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + **kwargs + ) + self._unilabos_state: ElectrodeSheetState = ElectrodeSheetState( + diameter=14, + thickness=0.1, + mass=0.5, + material_type="copper", + color="#8b4513", + 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 + + +def PositiveCan(name: str) -> ElectrodeSheet: + """创建正极壳""" + sheet = ElectrodeSheet(name=name, size_x=12, size_y=12, size_z=3.0, model="PositiveCan") + sheet.load_state({"diameter": 20.0, "thickness": 0.5, "mass": 0.5, "material_type": "aluminum", "color": electrode_colors["PositiveCan"], "info": None}) + return sheet + + +def PositiveElectrode(name: str) -> ElectrodeSheet: + """创建正极片""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.1, model="PositiveElectrode") + sheet.load_state({"material_type": "positive_electrode", "color": electrode_colors["PositiveElectrode"]}) + return sheet + + +def NegativeCan(name: str) -> ElectrodeSheet: + """创建负极壳""" + sheet = ElectrodeSheet(name=name, size_x=12, size_y=12, size_z=2.0, model="NegativeCan") + sheet.load_state({"material_type": "steel", "color": electrode_colors["NegativeCan"]}) + return sheet + + +def NegativeElectrode(name: str) -> ElectrodeSheet: + """创建负极片""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.1, model="NegativeElectrode") + sheet.load_state({"material_type": "negative_electrode", "color": electrode_colors["NegativeElectrode"]}) + return sheet + + +def SpringWasher(name: str) -> ElectrodeSheet: + """创建弹片""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.5, model="SpringWasher") + sheet.load_state({"material_type": "spring_steel", "color": electrode_colors["SpringWasher"]}) + return sheet + + +def FlatWasher(name: str) -> ElectrodeSheet: + """创建垫片""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.2, model="FlatWasher") + sheet.load_state({"material_type": "steel", "color": electrode_colors["FlatWasher"]}) + return sheet + + +def AluminumFoil(name: str) -> ElectrodeSheet: + """创建铝箔""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.05, model="AluminumFoil") + sheet.load_state({"material_type": "aluminum", "color": electrode_colors["AluminumFoil"]}) + return sheet + + +class BatteryState(TypedDict): + color: str # 材料类型对应的颜色 + electrolyte_name: str + data_electrolyte_code: str + open_circuit_voltage: float + assembly_pressure: float + electrolyte_volume: float + + info: Optional[str] # 附加信息 + + +class Battery(Container): + """电池类 - 包含组装好的电池""" + + def __init__( + self, + name: str = "电池", + size_x: float = 12, + size_y: float = 12, + size_z: float = 6, + category: str = "battery", + model: Optional[str] = None, + **kwargs + ): + """初始化电池 + + Args: + name: 电池名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + category: 类别 + model: 型号 + **kwargs: 其他参数传递给父类 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + **kwargs + ) + self._unilabos_state: BatteryState = BatteryState( + color=electrode_colors["Battery"], + electrolyte_name="无", + data_electrolyte_code="", + open_circuit_voltage=0.0, + assembly_pressure=0.0, + electrolyte_volume=0.0, + info=None + ) + + 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 \ No newline at end of file diff --git a/unilabos/resources/battery/magazine.py b/unilabos/resources/battery/magazine.py new file mode 100644 index 00000000..04328a40 --- /dev/null +++ b/unilabos/resources/battery/magazine.py @@ -0,0 +1,344 @@ +from typing import Dict, List, Optional, OrderedDict, Union, Callable +import math + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources import Resource, ResourceStack, ItemizedResource +from pylabrobot.resources.carrier import create_homogeneous_resources + +from unilabos.resources.battery.electrode_sheet import ( + PositiveCan, PositiveElectrode, + NegativeCan, NegativeElectrode, + SpringWasher, FlatWasher, + AluminumFoil, + Battery +) + + +class Magazine(ResourceStack): + """子弹夹洞位类""" + + def __init__( + self, + name: str, + direction: str = 'z', + resources: Optional[List[Resource]] = None, + max_sheets: int = 100, + **kwargs + ): + """初始化子弹夹洞位 + + Args: + name: 洞位名称 + direction: 堆叠方向 + resources: 资源列表 + max_sheets: 最大极片数量 + """ + super().__init__( + name=name, + direction=direction, + resources=resources, + ) + self.max_sheets = max_sheets + + @property + def size_x(self) -> float: + return self.get_size_x() + + @property + def size_y(self) -> float: + return self.get_size_y() + + @property + def size_z(self) -> float: + return self.get_size_z() + + def serialize(self) -> dict: + return { + **super().serialize(), + "size_x": self.size_x or 10.0, + "size_y": self.size_y or 10.0, + "size_z": self.size_z or 10.0, + "max_sheets": self.max_sheets, + } + + +class MagazineHolder(ItemizedResource): + """子弹夹类 - 有多个洞位,每个洞位放多个极片""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + ordered_items: Optional[Dict[str, Magazine]] = None, + ordering: Optional[OrderedDict[str, str]] = None, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + max_sheets_per_hole: int = 100, + cross_section_type: str = "circle", + category: str = "magazine_holder", + model: Optional[str] = None, + ): + """初始化子弹夹 + + Args: + name: 子弹夹名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + + 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, + ) + + # 保存洞位的直径和深度 + self.hole_diameter = hole_diameter + self.hole_depth = hole_depth + self.max_sheets_per_hole = max_sheets_per_hole + self.cross_section_type = cross_section_type + + 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, + "cross_section_type": self.cross_section_type, + } + + +def magazine_factory( + name: str, + size_x: float, + size_y: float, + size_z: float, + locations: List[Coordinate], + klasses: Optional[List[Callable[[str], str]]] = None, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + max_sheets_per_hole: int = 100, + category: str = "magazine_holder", + model: Optional[str] = None, +) -> 'MagazineHolder': + """工厂函数:创建子弹夹 + + Args: + name: 子弹夹名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + locations: 洞位坐标列表 + klasses: 每个洞位中极片的类列表 + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + for loc in locations: + loc.x -= hole_diameter / 2 + loc.y -= hole_diameter / 2 + + # 创建洞位 + _sites = create_homogeneous_resources( + klass=Magazine, + locations=locations, + resource_size_x=hole_diameter, + resource_size_y=hole_diameter, + name_prefix=name, + max_sheets=max_sheets_per_hole, + ) + + # 生成编号键 + keys = [f"A{i+1}" for i in range(len(locations))] + sites = dict(zip(keys, _sites.values())) + + holder = MagazineHolder( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=sites, + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category=category, + model=model, + ) + + if klasses is not None: + for i, klass in enumerate(klasses): + hole_key = keys[i] + hole = holder.children[i] + for j in reversed(range(max_sheets_per_hole)): + item_name = f"{hole_key}_sheet{j+1}" + item = klass(name=item_name) + hole.assign_child_resource(item) + return holder + + +def MagazineHolder_6_Cathode( + name: str, + size_x: float = 80.0, + size_y: float = 80.0, + size_z: float = 40.0, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 20.0, + max_sheets_per_hole: int = 100, +) -> MagazineHolder: + """创建6孔子弹夹 - 六边形排布""" + center_x = size_x / 2 + center_y = size_y / 2 + + locations = [] + + # 周围6个孔,按六边形排布 + for i in range(6): + angle = i * 60 * math.pi / 180 # 每60度一个孔 + x = center_x + hole_spacing * math.cos(angle) + y = center_y + hole_spacing * math.sin(angle) + locations.append(Coordinate(x, y, size_z - hole_depth)) + + return magazine_factory( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + locations=locations, + klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan], + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="magazine_holder", + model="MagazineHolder_6_Cathode", + ) + + +def MagazineHolder_6_Anode( + name: str, + size_x: float = 80.0, + size_y: float = 80.0, + size_z: float = 40.0, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 20.0, + max_sheets_per_hole: int = 100, +) -> MagazineHolder: + """创建6孔子弹夹 - 六边形排布""" + center_x = size_x / 2 + center_y = size_y / 2 + + locations = [] + + # 周围6个孔,按六边形排布 + for i in range(6): + angle = i * 60 * math.pi / 180 # 每60度一个孔 + x = center_x + hole_spacing * math.cos(angle) + y = center_y + hole_spacing * math.sin(angle) + locations.append(Coordinate(x, y, size_z - hole_depth)) + + return magazine_factory( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + locations=locations, + klasses=[SpringWasher, NegativeCan, NegativeCan, SpringWasher, NegativeCan, NegativeCan], + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="magazine_holder", + model="MagazineHolder_6_Anode", + ) + + +def MagazineHolder_6_Battery( + name: str, + size_x: float = 80.0, + size_y: float = 80.0, + size_z: float = 40.0, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 20.0, + max_sheets_per_hole: int = 100, +) -> MagazineHolder: + """创建6孔子弹夹 - 六边形排布""" + center_x = size_x / 2 + center_y = size_y / 2 + + locations = [] + + # 周围6个孔,按六边形排布 + for i in range(6): + angle = i * 60 * math.pi / 180 # 每60度一个孔 + x = center_x + hole_spacing * math.cos(angle) + y = center_y + hole_spacing * math.sin(angle) + locations.append(Coordinate(x, y, size_z - hole_depth)) + + return magazine_factory( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + locations=locations, + klasses=None, # 初始化时,不放入装好的电池 + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="magazine_holder", + model="MagazineHolder_6_Battery", + ) + + +def MagazineHolder_4_Cathode( + name: str, +) -> MagazineHolder: + """创建4孔子弹夹 - 正方形四角排布""" + size_x: float = 80.0 + size_y: float = 80.0 + size_z: float = 10.0 + hole_diameter: float = 14.0 + hole_depth: float = 10.0 + hole_spacing: float = 25.0 + max_sheets_per_hole: int = 100 + + # 计算4个洞位的坐标(正方形四角排布) + center_x = size_x / 2 + center_y = size_y / 2 + offset = hole_spacing / 2 + + locations = [ + Coordinate(center_x - offset, center_y - offset, size_z - hole_depth), # 左下 + Coordinate(center_x + offset, center_y - offset, size_z - hole_depth), # 右下 + Coordinate(center_x - offset, center_y + offset, size_z - hole_depth), # 左上 + Coordinate(center_x + offset, center_y + offset, size_z - hole_depth), # 右上 + ] + + return magazine_factory( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + locations=locations, + klasses=[AluminumFoil, PositiveElectrode, PositiveElectrode, PositiveElectrode], + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="magazine_holder", + model="MagazineHolder_4_Cathode", + ) diff --git a/unilabos/resources/bioyond/README_WAREHOUSE.md b/unilabos/resources/bioyond/README_WAREHOUSE.md new file mode 100644 index 00000000..6d1a62e1 --- /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_bottle_carriers.py b/unilabos/resources/bioyond/YB_bottle_carriers.py new file mode 100644 index 00000000..29a53242 --- /dev/null +++ b/unilabos/resources/bioyond/YB_bottle_carriers.py @@ -0,0 +1,653 @@ +from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d + +from unilabos.resources.itemized_carrier import Bottle, BottleCarrier +from unilabos.resources.bioyond.YB_bottles import ( + YB_jia_yang_tou_da, + YB_ye_Bottle, + YB_ye_100ml_Bottle, + YB_gao_nian_ye_Bottle, + YB_5ml_fenyeping, + YB_20ml_fenyeping, + YB_pei_ye_xiao_Bottle, + YB_pei_ye_da_Bottle, + YB_qiang_tou, +) +# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial + + +def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 30.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=3, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="Electrolyte_6VialCarrier", + ) + carrier.num_items_x = 3 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + # for i in range(6): + # carrier[i] = YB_Solid_Vial(f"{name}_vial_{i+1}") + return carrier + + +def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 100.0 + + # 烧杯尺寸 + beaker_diameter = 80.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="Electrolyte_1BottleCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + # carrier[0] = YB_Solution_Beaker(f"{name}_beaker_1") + return carrier + + +def YB_6StockCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 20.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=3, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="6StockCarrier", + ) + carrier.num_items_x = 3 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序 + # for i in range(6): + # carrier[i] = YB_Solid_Stock(f"{name}_vial_{ordering[i]}") + return carrier + + +def YB_6VialCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 30.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=3, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="6VialCarrier", + ) + carrier.num_items_x = 3 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序 + # for i in range(3): + # carrier[i] = YB_Solid_Vial(f"{name}_solidvial_{ordering[i]}") + # for i in range(3, 6): + # carrier[i] = YB_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}") + return carrier + +# 1瓶载架 - 单个中央位置 +def YB_ye(name: str) -> BottleCarrier: + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 20.0 + + # 烧杯尺寸 + beaker_diameter = 60.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="YB_ye", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_ye_Bottle(f"{name}_flask_1") + return carrier + + +# 高粘液瓶载架 - 单个中央位置 +def YB_gaonianye(name: str) -> BottleCarrier: + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 20.0 + + # 烧杯尺寸 + beaker_diameter = 60.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="YB_gaonianye", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1") + return carrier + + +# 100ml液体瓶载架 - 单个中央位置 +def YB_100ml_yeti(name: str) -> BottleCarrier: + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 20.0 + + # 烧杯尺寸 + beaker_diameter = 60.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="YB_100ml_yeti", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1") + return carrier + +# 5ml分液瓶板 - 4x2布局,8个位置 +def YB_5ml_fenyepingban(name: str) -> BottleCarrier: + + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 15.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=4, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="YB_5ml_fenyepingban", + ) + carrier.num_items_x = 4 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] + for i in range(8): + carrier[i] = YB_5ml_fenyeping(f"{name}_vial_{ordering[i]}") + return carrier + +# 20ml分液瓶板 - 4x2布局,8个位置 +def YB_20ml_fenyepingban(name: str) -> BottleCarrier: + + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 70.0 + + # 瓶位尺寸 + bottle_diameter = 20.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=4, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="YB_20ml_fenyepingban", + ) + carrier.num_items_x = 4 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] + for i in range(8): + carrier[i] = YB_20ml_fenyeping(f"{name}_vial_{ordering[i]}") + return carrier + +# 配液瓶(小)板 - 4x2布局,8个位置 +def YB_peiyepingxiaoban(name: str) -> BottleCarrier: + + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 65.0 + + # 瓶位尺寸 + bottle_diameter = 35.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=4, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="YB_peiyepingxiaoban", + ) + carrier.num_items_x = 4 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] + for i in range(8): + carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}") + return carrier + + +# 配液瓶(大)板 - 2x2布局,4个位置 +def YB_peiyepingdaban(name: str) -> BottleCarrier: + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 95.0 + + # 瓶位尺寸 + bottle_diameter = 55.0 + bottle_spacing_x = 60.0 # X方向间距 + bottle_spacing_y = 60.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=2, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="YB_peiyepingdaban", + ) + carrier.num_items_x = 2 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "B1", "B2"] + for i in range(4): + carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}") + return carrier + +# 加样头(大)板 - 1x1布局,1个位置 +def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier: + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 95.0 + + # 瓶位尺寸 + bottle_diameter = 35.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (1 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (1 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=1, + num_items_y=1, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="YB_jia_yang_tou_da_Carrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_jia_yang_tou_da(f"{name}_head_1") + return carrier + + +def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier: + """适配器块 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 30.0 + + # 适配器尺寸 + adapter_diameter = 80.0 + + # 计算中央位置 + center_x = (carrier_size_x - adapter_diameter) / 2 + center_y = (carrier_size_y - adapter_diameter) / 2 + center_z = 0.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=adapter_diameter, + resource_size_y=adapter_diameter, + name_prefix=name, + ), + model="YB_shi_pei_qi_kuai", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + # 适配器块本身不包含瓶子,只是一个支撑结构 + return carrier + + +def YB_qiang_tou_he(name: str) -> BottleCarrier: + """枪头盒 - 8x12布局,96个位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 55.0 + + # 枪头尺寸 + tip_diameter = 10.0 + tip_spacing_x = 9.0 # X方向间距 + tip_spacing_y = 9.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2 + start_y = (carrier_size_y - (8 - 1) * tip_spacing_y - tip_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=12, + num_items_y=8, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=tip_spacing_x, + item_dy=tip_spacing_y, + size_x=tip_diameter, + size_y=tip_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="YB_qiang_tou_he", + ) + carrier.num_items_x = 12 + carrier.num_items_y = 8 + carrier.num_items_z = 1 + # 创建96个枪头 + for i in range(96): + row = chr(65 + i // 12) # A-H + col = (i % 12) + 1 # 1-12 + carrier[i] = YB_qiang_tou(f"{name}_tip_{row}{col}") + return carrier + diff --git a/unilabos/resources/bioyond/YB_bottles.py b/unilabos/resources/bioyond/YB_bottles.py new file mode 100644 index 00000000..acbbf35b --- /dev/null +++ b/unilabos/resources/bioyond/YB_bottles.py @@ -0,0 +1,163 @@ +from unilabos.resources.itemized_carrier import Bottle, BottleCarrier +# 工厂函数 +"""加样头(大)""" +def YB_jia_yang_tou_da( + name: str, + diameter: float = 20.0, + height: float = 100.0, + max_volume: float = 30000.0, # 30mL + barcode: str = None, +) -> Bottle: + """创建粉末瓶""" + return Bottle( + name=name, + diameter=diameter,# 未知 + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_jia_yang_tou_da", + ) + +"""液1x1""" +def YB_ye_Bottle( + name: str, + diameter: float = 40.0, + height: float = 70.0, + max_volume: float = 50000.0, # 50mL + barcode: str = None, +) -> Bottle: + """创建液体瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_ye_Bottle", + ) + +"""100ml液体""" +def YB_ye_100ml_Bottle( + name: str, + diameter: float = 50.0, + height: float = 90.0, + max_volume: float = 100000.0, # 100mL + barcode: str = None, +) -> Bottle: + """创建100ml液体瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_100ml_yeti", + ) + +"""高粘液""" +def YB_gao_nian_ye_Bottle( + name: str, + diameter: float = 40.0, + height: float = 70.0, + max_volume: float = 50000.0, # 50mL + barcode: str = None, +) -> Bottle: + """创建高粘液瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="High_Viscosity_Liquid", + ) + +"""5ml分液瓶""" +def YB_5ml_fenyeping( + name: str, + diameter: float = 20.0, + height: float = 50.0, + max_volume: float = 5000.0, # 5mL + barcode: str = None, +) -> Bottle: + """创建5ml分液瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_5ml_fenyeping", + ) + +"""20ml分液瓶""" +def YB_20ml_fenyeping( + name: str, + diameter: float = 30.0, + height: float = 65.0, + max_volume: float = 20000.0, # 20mL + barcode: str = None, +) -> Bottle: + """创建20ml分液瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_20ml_fenyeping", + ) + +"""配液瓶(小)""" +def YB_pei_ye_xiao_Bottle( + name: str, + diameter: float = 35.0, + height: float = 60.0, + max_volume: float = 30000.0, # 30mL + barcode: str = None, +) -> Bottle: + """创建配液瓶(小)""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_pei_ye_xiao_Bottle", + ) + +"""配液瓶(大)""" +def YB_pei_ye_da_Bottle( + name: str, + diameter: float = 55.0, + height: float = 100.0, + max_volume: float = 150000.0, # 150mL + barcode: str = None, +) -> Bottle: + """创建配液瓶(大)""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_pei_ye_da_Bottle", + ) + +"""枪头""" +def YB_qiang_tou( + name: str, + diameter: float = 10.0, + height: float = 50.0, + max_volume: float = 1000.0, # 1mL + barcode: str = None, +) -> Bottle: + """创建枪头""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_qiang_tou", + ) diff --git a/unilabos/resources/bioyond/YB_warehouses.py b/unilabos/resources/bioyond/YB_warehouses.py new file mode 100644 index 00000000..7b3ab510 --- /dev/null +++ b/unilabos/resources/bioyond/YB_warehouses.py @@ -0,0 +1,384 @@ +from unilabos.resources.warehouse import WareHouse, warehouse_factory + +# ================ 反应站相关堆栈 ================ + +def bioyond_warehouse_1x4x4(name: str) -> WareHouse: + """创建BioYond 4x4x1仓库 (左侧堆栈: A01~D04) + + 使用行优先排序,前端展示为: + A01 | A02 | A03 | A04 + B01 | B02 | B03 | B04 + C01 | C02 | C03 | C04 + D01 | D02 | D03 | D04 + """ + return warehouse_factory( + name=name, + num_items_x=4, # 4列 + num_items_y=4, # 4行 + num_items_z=1, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=147.0, + item_dy=106.0, + item_dz=130.0, + category="warehouse", + col_offset=0, # 从01开始: A01, A02, A03, A04 + layout="row-major", # ⭐ 改为行优先排序 + ) + +def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse: + """创建BioYond 4x4x1仓库 (右侧堆栈: A05~D08)""" + return warehouse_factory( + name=name, + num_items_x=4, + num_items_y=4, + num_items_z=1, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=147.0, + item_dy=106.0, + item_dz=130.0, + category="warehouse", + col_offset=4, # 从05开始: A05, A06, A07, A08 + layout="row-major", # ⭐ 改为行优先排序 + ) + +def bioyond_warehouse_density_vial(name: str) -> WareHouse: + """创建测量小瓶仓库(测密度) A01~B03""" + return warehouse_factory( + name=name, + num_items_x=3, # 3列(01-03) + num_items_y=2, # 2行(A-B) + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=40.0, + item_dy=40.0, + item_dz=50.0, + # 用更小的 resource_size 来表现 "小点的孔位" + resource_size_x=30.0, + resource_size_y=30.0, + resource_size_z=12.0, + category="warehouse", + col_offset=0, + layout="row-major", + ) + +def bioyond_warehouse_reagent_storage(name: str) -> WareHouse: + """创建BioYond站内试剂存放堆栈(A01~A02, 1行×2列)""" + return warehouse_factory( + name=name, + num_items_x=2, # 2列(01-02) + num_items_y=1, # 1行(A) + 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", + ) + +def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse: + """创建BioYond站内Tip盒堆栈(A01~B03, 2行×3列)""" + return warehouse_factory( + name=name, + num_items_x=3, # 3列(01-03) + num_items_y=2, # 2行(A-B) + 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", + col_offset=0, + layout="row-major", + ) + +def bioyond_warehouse_liquid_preparation(name: str) -> WareHouse: + """已弃用,创建BioYond移液站内10%分装液体准备仓库(A01~B04)""" + return warehouse_factory( + name=name, + num_items_x=4, # 4列(01-04) + num_items_y=2, # 2行(A-B) + 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", + col_offset=0, + layout="row-major", + ) + +# ================ 配液站相关堆栈 ================ + +def bioyond_warehouse_reagent_stack(name: str) -> WareHouse: + """创建BioYond 试剂堆栈 2x4x1 (2行×4列: A01-A04, B01-B04) + + 使用行优先排序,前端展示为: + A01 | A02 | A03 | A04 + B01 | B02 | B03 | B04 + """ + return warehouse_factory( + name=name, + num_items_x=4, # 4列 (01-04) + num_items_y=2, # 2行 (A-B) + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=147.0, + item_dy=106.0, + item_dz=130.0, + category="warehouse", + col_offset=0, # 从01开始 + layout="row-major", # ⭐ 使用行优先排序: A01,A02,A03,A04, B01,B02,B03,B04 + ) + + # 定义bioyond的堆栈 + +# =================== Other =================== + +def bioyond_warehouse_1x4x2(name: str) -> WareHouse: + """创建BioYond 4x2x1仓库""" + return warehouse_factory( + name=name, + num_items_x=1, + num_items_y=4, + num_items_z=2, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + removed_positions=None + ) + +def bioyond_warehouse_1x2x2(name: str) -> WareHouse: + """创建BioYond 1x2x2仓库(1列×2行×2层)- 旧版本,已弃用 + + 布局(2层): + 层1: A01 + B01 + 层2: A02 + B02 + """ + return warehouse_factory( + name=name, + num_items_x=1, + num_items_y=2, + num_items_z=2, + 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_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( + name=name, + num_items_x=10, + num_items_y=1, + 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_1x3x3(name: str) -> WareHouse: + """创建BioYond 1x3x3仓库""" + return warehouse_factory( + name=name, + num_items_x=1, + num_items_y=3, + num_items_z=3, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.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仓库(3行×3列×1层) + + 布局: + A01 | A02 | A03 + B01 | B02 | B03 + C01 | C02 | C03 + """ + 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", + layout="row-major", # ⭐ 使用行优先避免上下颠倒 + ) +def bioyond_warehouse_2x1x3(name: str) -> WareHouse: + """创建BioYond 2x1x3仓库""" + return warehouse_factory( + name=name, + num_items_x=2, + num_items_y=1, + num_items_z=3, + 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_5x1x1(name: str) -> WareHouse: + """已弃用:创建BioYond 5x1x1仓库""" + return warehouse_factory( + name=name, + num_items_x=5, + num_items_y=1, + 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_3x3x1_2(name: str) -> WareHouse: + """已弃用:创建BioYond 3x3x1仓库""" + return warehouse_factory( + name=name, + num_items_x=3, + num_items_y=3, + num_items_z=1, + dx=12.0, + dy=12.0, + dz=12.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) + +def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse: + """创建BioYond开关盖加液模块台面""" + return warehouse_factory( + name=name, + num_items_x=2, + num_items_y=5, + 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", + removed_positions=None + ) + +def bioyond_warehouse_1x8x4(name: str) -> WareHouse: + """创建BioYond 8x4x1反应站堆栈(A01~D08)""" + return warehouse_factory( + name=name, + num_items_x=8, # 8列(01-08) + num_items_y=4, # 4行(A-D) + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=147.0, + item_dy=106.0, + item_dz=130.0, + category="warehouse", + ) diff --git a/unilabos/resources/bioyond/bottles.py b/unilabos/resources/bioyond/bottles.py index d60d65ab..7045d8b7 100644 --- a/unilabos/resources/bioyond/bottles.py +++ b/unilabos/resources/bioyond/bottles.py @@ -193,3 +193,20 @@ def BIOYOND_PolymerStation_Flask( barcode=barcode, model="BIOYOND_PolymerStation_Flask", ) + +def BIOYOND_PolymerStation_Measurement_Vial( + name: str, + diameter: float = 25.0, + height: float = 60.0, + max_volume: float = 20000.0, # 20mL + barcode: str = None, +) -> Bottle: + """创建测量小瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="BIOYOND_PolymerStation_Measurement_Vial", + ) diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index 5572536b..5f3b2c4e 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -1,14 +1,16 @@ from os import name from pylabrobot.resources import Deck, Coordinate, Rotation -from unilabos.resources.bioyond.warehouses import ( +from unilabos.resources.bioyond.YB_warehouses import ( bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x4_right, # 新增:右侧仓库 (A05~D08) bioyond_warehouse_1x4x2, 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, @@ -16,9 +18,12 @@ bioyond_warehouse_1x8x4, bioyond_warehouse_reagent_storage, # bioyond_warehouse_liquid_preparation, - bioyond_warehouse_tipbox_storage, # 新增:Tip盒堆栈 bioyond_warehouse_density_vial, ) +from unilabos.resources.bioyond.warehouses import ( + bioyond_warehouse_tipbox_storage_left, # 新增:Tip盒堆栈(左) + bioyond_warehouse_tipbox_storage_right, # 新增:Tip盒堆栈(右) +) class BIOYOND_PolymerReactionStation_Deck(Deck): @@ -45,24 +50,22 @@ def setup(self) -> None: "堆栈1右": bioyond_warehouse_1x4x4_right("堆栈1右"), # 右侧堆栈: A05~D08 "站内试剂存放堆栈": bioyond_warehouse_reagent_storage("站内试剂存放堆栈"), # A01~A02 # "移液站内10%分装液体准备仓库": bioyond_warehouse_liquid_preparation("移液站内10%分装液体准备仓库"), # A01~B04 - "站内Tip盒堆栈": bioyond_warehouse_tipbox_storage("站内Tip盒堆栈"), # A01~B03, 存放枪头盒. + "站内Tip盒堆栈(左)": bioyond_warehouse_tipbox_storage_left("站内Tip盒堆栈(左)"), # A02~B03 + "站内Tip盒堆栈(右)": bioyond_warehouse_tipbox_storage_right("站内Tip盒堆栈(右)"), # A01~B01 "测量小瓶仓库(测密度)": bioyond_warehouse_density_vial("测量小瓶仓库(测密度)"), # A01~B03 } self.warehouse_locations = { - "堆栈1左": Coordinate(0.0, 430.0, 0.0), # 左侧位置 - "堆栈1右": Coordinate(2500.0, 430.0, 0.0), # 右侧位置 - "站内试剂存放堆栈": Coordinate(640.0, 480.0, 0.0), - # "移液站内10%分装液体准备仓库": Coordinate(1200.0, 600.0, 0.0), - "站内Tip盒堆栈": Coordinate(300.0, 150.0, 0.0), - "测量小瓶仓库(测密度)": Coordinate(922.0, 552.0, 0.0), + "堆栈1左": Coordinate(-200.0, 400.0, 0.0), # 左侧位置 + "堆栈1右": Coordinate(2350.0, 400.0, 0.0), # 右侧位置 + "站内试剂存放堆栈": Coordinate(640.0, 400.0, 0.0), + "站内Tip盒堆栈(左)": Coordinate(300.0, 100.0, 0.0), + "站内Tip盒堆栈(右)": Coordinate(2250.0, 100.0, 0.0), # 向右偏移 2 * item_dx (137.0) + "测量小瓶仓库(测密度)": Coordinate(1000.0, 530.0, 0.0), } - self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90) - self.warehouses["测量小瓶仓库(测密度)"].rotation = Rotation(z=270) for warehouse_name, warehouse in self.warehouses.items(): self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) - class BIOYOND_PolymerPreparationStation_Deck(Deck): def __init__( self, @@ -90,9 +93,9 @@ def setup(self) -> None: "溶液堆栈": bioyond_warehouse_1x4x4("溶液堆栈"), # 4行×4列 (A01-D04) } self.warehouse_locations = { - "粉末堆栈": Coordinate(0.0, 450.0, 0.0), - "试剂堆栈": Coordinate(1850.0, 200.0, 0.0), - "溶液堆栈": Coordinate(2500.0, 450.0, 0.0), + "粉末堆栈": Coordinate(-200.0, 400.0, 0.0), + "试剂堆栈": Coordinate(1750.0, 160.0, 0.0), + "溶液堆栈": Coordinate(2350.0, 400.0, 0.0), } for warehouse_name, warehouse in self.warehouses.items(): @@ -115,10 +118,10 @@ def __init__( 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("加样头堆栈右"), @@ -126,6 +129,7 @@ def setup(self) -> None: "母液加样右": bioyond_warehouse_3x3x1_2("母液加样右"), "大瓶母液堆栈左": bioyond_warehouse_5x1x1("大瓶母液堆栈左"), "大瓶母液堆栈右": bioyond_warehouse_5x1x1("大瓶母液堆栈右"), + "2号手套箱内部堆栈": bioyond_warehouse_3x3x1("2号手套箱内部堆栈"), # 新增:3行×3列 (A01-C03) } # warehouse 的位置 self.warehouse_locations = { @@ -140,10 +144,12 @@ def setup(self) -> None: "母液加样右": Coordinate(2152.0, 333.0, 0.0), "大瓶母液堆栈左": Coordinate(1164.0, 676.0, 0.0), "大瓶母液堆栈右": Coordinate(2717.0, 676.0, 0.0), + "2号手套箱内部堆栈": Coordinate(-800, -500.0, 0.0), # 新增:位置需根据实际硬件调整 } for warehouse_name, warehouse in self.warehouses.items(): self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) + def YB_Deck(name: str) -> Deck: by=BIOYOND_YB_Deck(name=name) by.setup() diff --git a/unilabos/resources/bioyond/warehouses.py b/unilabos/resources/bioyond/warehouses.py index ae9e473d..547af1e3 100644 --- a/unilabos/resources/bioyond/warehouses.py +++ b/unilabos/resources/bioyond/warehouses.py @@ -46,33 +46,63 @@ def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse: ) def bioyond_warehouse_density_vial(name: str) -> WareHouse: - """创建测量小瓶仓库(测密度) A01~B03""" + """创建测量小瓶仓库(测密度) - 竖向排列2列3行 + 布局(从下到上,从左到右): + | A03 | B03 | ← 顶部 + | A02 | B02 | ← 中部 + | A01 | B01 | ← 底部 + """ return warehouse_factory( name=name, - num_items_x=3, # 3列(01-03) - num_items_y=2, # 2行(A-B) + num_items_x=2, # 2列(A, B) + num_items_y=3, # 3行(01-03,从下到上) num_items_z=1, # 1层 dx=10.0, dy=10.0, dz=10.0, - item_dx=40.0, - item_dy=40.0, + item_dx=40.0, # 列间距(A到B的横向距离) + item_dy=40.0, # 行间距(01到02到03的竖向距离) item_dz=50.0, - # 用更小的 resource_size 来表现 "小点的孔位" + # ⭐ 竖向warehouse:槽位尺寸也是竖向的(小瓶已经是正方形,无需调整) resource_size_x=30.0, resource_size_y=30.0, resource_size_z=12.0, category="warehouse", col_offset=0, - layout="row-major", + layout="vertical-col-major", # ⭐ 竖向warehouse专用布局 ) def bioyond_warehouse_reagent_storage(name: str) -> WareHouse: - """创建BioYond站内试剂存放堆栈(A01~A02, 1行×2列)""" + """创建BioYond站内试剂存放堆栈 - 竖向排列1列2行 + 布局(竖向,从下到上): + | A02 | ← 顶部 + | A01 | ← 底部 + """ return warehouse_factory( name=name, - num_items_x=2, # 2列(01-02) - num_items_y=1, # 1行(A) + num_items_x=1, # 1列 + num_items_y=2, # 2行(01-02,从下到上) + num_items_z=1, # 1层 + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=96.0, # 列间距(这里只有1列,不重要) + item_dy=137.0, # 行间距(A01到A02的竖向距离) + item_dz=120.0, + # ⭐ 竖向warehouse:交换槽位尺寸,使槽位框也是竖向的 + resource_size_x=86.0, # 原来的 resource_size_y + resource_size_y=127.0, # 原来的 resource_size_x + resource_size_z=25.0, + category="warehouse", + layout="vertical-col-major", # ⭐ 竖向warehouse专用布局 + ) + +def bioyond_warehouse_tipbox_storage_left(name: str) -> WareHouse: + """创建BioYond站内Tip盒堆栈左侧部分(A02~B03),2列2行""" + return warehouse_factory( + name=name, + num_items_x=2, # 2列 + num_items_y=2, # 2行(A-B) num_items_z=1, # 1层 dx=10.0, dy=10.0, @@ -81,13 +111,15 @@ def bioyond_warehouse_reagent_storage(name: str) -> WareHouse: item_dy=96.0, item_dz=120.0, category="warehouse", + col_offset=1, # 从02开始: A02, A03 + layout="row-major", ) -def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse: - """创建BioYond站内Tip盒堆栈(A01~B03),用于存放枪头盒""" +def bioyond_warehouse_tipbox_storage_right(name: str) -> WareHouse: + """创建BioYond站内Tip盒堆栈右侧部分(A01~B01),1列2行""" return warehouse_factory( name=name, - num_items_x=3, # 3列(01-03) + num_items_x=1, # 1列 num_items_y=2, # 2行(A-B) num_items_z=1, # 1层 dx=10.0, @@ -97,7 +129,7 @@ def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse: item_dy=96.0, item_dz=120.0, category="warehouse", - col_offset=0, + col_offset=0, # 从01开始: A01 layout="row-major", ) diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 252c8f45..1c514d44 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -779,6 +779,22 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st if not locations: logger.debug(f"[物料位置] {unique_name} 没有location信息,跳过warehouse放置") + # ⭐ 预先检查:如果物料的任何location在竖向warehouse中,提前交换尺寸 + # 这样可以避免多个location时尺寸不一致的问题 + needs_size_swap = False + for loc in locations: + wh_name_check = loc.get("whName") + if wh_name_check in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]: + needs_size_swap = True + break + + if needs_size_swap and hasattr(plr_material, 'size_x') and hasattr(plr_material, 'size_y'): + original_x = plr_material.size_x + original_y = plr_material.size_y + plr_material.size_x = original_y + plr_material.size_y = original_x + logger.debug(f" 物料 {unique_name} 将放入竖向warehouse,预先交换尺寸: {original_x}×{original_y} → {plr_material.size_x}×{plr_material.size_y}") + for loc in locations: wh_name = loc.get("whName") logger.debug(f"[物料位置] {unique_name} 尝试放置到 warehouse: {wh_name} (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')}, z={loc.get('z')})") @@ -795,12 +811,20 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st logger.warning(f"物料 {material['name']} 的列号 x={x_val} 超出范围,无法映射到堆栈1左或堆栈1右") continue + # 特殊处理: Bioyond的"站内Tip盒堆栈"也需要进行拆分映射 + if wh_name == "站内Tip盒堆栈": + y_val = loc.get("y", 1) + if y_val == 1: + wh_name = "站内Tip盒堆栈(右)" + elif y_val in [2, 3]: + wh_name = "站内Tip盒堆栈(左)" + y = y - 1 # 调整列号,因为左侧仓库对应的 Bioyond y=2 实际上是它的第1列 + if hasattr(deck, "warehouses") and wh_name in deck.warehouses: warehouse = deck.warehouses[wh_name] logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})") # Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1) - # PyLabRobot warehouse是列优先存储: A01,B01,C01,D01, A02,B02,C02,D02, ... x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D) y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...) z = loc.get("z", 1) # 层号 (1-based, 通常为1) @@ -809,12 +833,23 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st if wh_name == "堆栈1右": y = y - 4 # 将5-8映射到1-4 - # 特殊处理:对于1行×N列的横向warehouse(如站内试剂存放堆栈) - # Bioyond的y坐标表示线性位置序号,而不是列号 - if warehouse.num_items_y == 1: - # 1行warehouse: 直接用y作为线性索引 - idx = y - 1 - logger.debug(f"1行warehouse {wh_name}: y={y} → idx={idx}") + # 特殊处理竖向warehouse(站内试剂存放堆栈、测量小瓶仓库) + # 这些warehouse使用 vertical-col-major 布局 + if wh_name in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]: + # vertical-col-major 布局的坐标映射: + # - Bioyond的x(1=A,2=B)对应warehouse的列(col, x方向) + # - Bioyond的y(1=01,2=02,3=03)对应warehouse的行(row, y方向),从下到上 + # vertical-col-major 中: row=0 对应底部,row=n-1 对应顶部 + # Bioyond y=1(01) 对应底部 → row=0, y=2(02) 对应中间 → row=1 + # 索引计算: idx = row * num_cols + col + col_idx = x - 1 # Bioyond的x(A,B) → col索引(0,1) + row_idx = y - 1 # Bioyond的y(01,02,03) → row索引(0,1,2) + layer_idx = z - 1 + + idx = layer_idx * (warehouse.num_items_x * warehouse.num_items_y) + row_idx * warehouse.num_items_y + col_idx + logger.debug(f"🔍 竖向warehouse {wh_name}: Bioyond(x={x},y={y},z={z}) → warehouse(col={col_idx},row={row_idx},layer={layer_idx}) → idx={idx}, capacity={warehouse.capacity}") + + # 普通横向warehouse的处理 else: # 多行warehouse: 根据 layout 使用不同的索引计算 row_idx = x - 1 # x表示行: 转为0-based @@ -838,6 +873,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st if 0 <= idx < warehouse.capacity: if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder): + # 物料尺寸已在放入warehouse前根据需要进行了交换 warehouse[idx] = plr_material logger.debug(f"✅ 物料 {unique_name} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})") else: @@ -1011,11 +1047,24 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict logger.debug(f" 📭 [单瓶物料] {resource.name} 无液体,使用资源名: {material_name}") # 🎯 处理物料默认参数和单位 - # 检查是否有该物料名称的默认参数配置 + # 优先级: typeId参数 > 物料名称参数 > 默认值 default_unit = "个" # 默认单位 material_parameters = {} - if material_name in material_params: + # 1️⃣ 首先检查是否有 typeId 对应的参数配置(从 material_params 中获取,key 格式为 "type:") + type_params_key = f"type:{type_id}" + if type_params_key in material_params: + params_config = material_params[type_params_key].copy() + + # 提取 unit 字段(如果有) + if "unit" in params_config: + default_unit = params_config.pop("unit") # 从参数中移除,放到外层 + + # 剩余的字段放入 Parameters + material_parameters = params_config + logger.debug(f" 🔧 [物料参数-按typeId] 为 typeId={type_id[:8]}... 应用配置: unit={default_unit}, parameters={material_parameters}") + # 2️⃣ 其次检查是否有该物料名称的默认参数配置 + elif material_name in material_params: params_config = material_params[material_name].copy() # 提取 unit 字段(如果有) @@ -1024,7 +1073,7 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict # 剩余的字段放入 Parameters material_parameters = params_config - logger.debug(f" 🔧 [物料参数] 为 {material_name} 应用配置: unit={default_unit}, parameters={material_parameters}") + logger.debug(f" 🔧 [物料参数-按名称] 为 {material_name} 应用配置: unit={default_unit}, parameters={material_parameters}") # 转换为 JSON 字符串 parameters_json = json.dumps(material_parameters) if material_parameters else "{}" diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index a5207d4c..fe55c39e 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -29,7 +29,7 @@ def __init__( size_x: float = 0.0, size_y: float = 0.0, size_z: float = 0.0, - barcode: Optional[str] = "", + barcode: Optional[str] = None, category: str = "container", model: Optional[str] = None, **kwargs, @@ -50,13 +50,45 @@ def __init__( self.barcode = barcode def serialize(self) -> dict: + # Pylabrobot expects barcode to be an object with serialize(), but here it is a str. + # We temporarily unset it to avoid AttributeError in super().serialize(). + _barcode = self.barcode + self.barcode = None + try: + data = super().serialize() + finally: + self.barcode = _barcode + return { - **super().serialize(), + **data, "diameter": self.diameter, "height": self.height, - "barcode": self.barcode, } + @classmethod + def deserialize(cls, data: dict, allow_marshal: bool = False): + # Extract barcode before calling parent deserialize to avoid type error + barcode_data = data.pop("barcode", None) + + # Call parent deserialize + instance = super(Bottle, cls).deserialize(data, allow_marshal=allow_marshal) + + # Set barcode as string (not as Barcode object) + if barcode_data: + if isinstance(barcode_data, str): + instance.barcode = barcode_data + elif isinstance(barcode_data, dict): + # If it's a dict (Barcode serialized format), extract the data field + instance.barcode = barcode_data.get("data", "") + else: + instance.barcode = "" + + # Set additional attributes + instance.diameter = data.get("diameter", instance._size_x) + instance.height = data.get("height", instance._size_z) + + return instance + T = TypeVar("T", bound=ResourceHolder) S = TypeVar("S", bound=ResourceHolder) diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index afd4c3b9..4097782d 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -621,6 +621,16 @@ def root_nodes(self) -> List[ResourceDictInstance]: """ return [tree.root_node for tree in self.trees] + @property + def root_nodes_uuid(self) -> List[ResourceDictInstance]: + """ + 获取所有树的根节点 + + Returns: + 所有根节点的资源实例列表 + """ + return [tree.root_node.res_content.uuid for tree in self.trees] + @property def all_nodes(self) -> List[ResourceDictInstance]: """ diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py index 4dcda6d9..929a4e4d 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"=行优先 ): # 创建位置坐标 @@ -42,6 +43,10 @@ def warehouse_factory( if layout == "row-major": # 行优先:row=0(A行) 应该显示在上方,需要较小的 y 值 y = dy + row * item_dy + elif layout == "vertical-col-major": + # 竖向warehouse: row=0 对应顶部(y小),row=n-1 对应底部(y大) + # 但标签 01 应该在底部,所以使用反向映射 + y = dy + (num_items_y - row - 1) * item_dy else: # 列优先:保持原逻辑(row=0 对应较大的 y) y = dy + (num_items_y - row - 1) * item_dy @@ -65,10 +70,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())} diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index c99df6c4..8a880233 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -622,7 +622,7 @@ async def get_resource(self, resources_uuid: List[str], with_children: bool = Tr ) # type: ignore raw_nodes = json.loads(response.response) tree_set = ResourceTreeSet.from_raw_dict_list(raw_nodes) - self.lab_logger().debug(f"获取资源结果: {len(tree_set.trees)} 个资源树") + self.lab_logger().trace(f"获取资源结果: {len(tree_set.trees)} 个资源树 {tree_set.root_nodes}") return tree_set async def get_resource_with_dir(self, resource_id: str, with_children: bool = True) -> "ResourcePLR": @@ -920,7 +920,14 @@ def _handle_update( plr_resources = tree_set.to_plr_resources() result, parents = _handle_add(plr_resources, tree_set, additional_add_params) parents: List[Optional["ResourcePLR"]] = [i for i in parents if i is not None] - de_dupe_parents = list(set(parents)) + # de_dupe_parents = list(set(parents)) + # Fix unhashable type error for WareHouse + de_dupe_parents = [] + _seen_ids = set() + for p in parents: + if id(p) not in _seen_ids: + _seen_ids.add(id(p)) + de_dupe_parents.append(p) new_tree_set = ResourceTreeSet.from_plr_resources(de_dupe_parents) # 去重 for tree in new_tree_set.trees: if tree.root_node.res_content.uuid_parent is None and self.node_name != "host_node": @@ -1565,16 +1572,16 @@ def _execute_driver_command(self, string: str): def _convert_resources_sync(self, *uuids: str) -> List["ResourcePLR"]: """同步转换资源 UUID 为实例 - + Args: *uuids: 一个或多个资源 UUID - + Returns: 单个 UUID 时返回单个资源实例,多个 UUID 时返回资源实例列表 """ if not uuids: raise ValueError("至少需要提供一个 UUID") - + uuids_list = list(uuids) future = self._resource_clients["c2s_update_resource_tree"].call_async(SerialCommand.Request( command=json.dumps( diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index cf9fd704..101476a1 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -735,7 +735,7 @@ def property_callback(self, msg, device_id: str, property_name: str) -> None: if bCreate: self.lab_logger().trace(f"Status created: {device_id}.{property_name} = {msg.data}") else: - self.lab_logger().debug(f"Status updated: {device_id}.{property_name} = {msg.data}") + self.lab_logger().trace(f"Status updated: {device_id}.{property_name} = {msg.data}") def send_goal( self, diff --git a/unilabos/test/experiments/dispensing_station_bioyond.json b/unilabos/test/experiments/dispensing_station_bioyond.json index 0be4129a..a6bd5332 100644 --- a/unilabos/test/experiments/dispensing_station_bioyond.json +++ b/unilabos/test/experiments/dispensing_station_bioyond.json @@ -9,49 +9,125 @@ "parent": null, "type": "device", "class": "bioyond_dispensing_station", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, "config": { - "config": { - "api_key": "DE9BDDA0", - "api_host": "http://192.168.1.200:44388", - "material_type_mappings": { - "BIOYOND_PolymerStation_1FlaskCarrier": [ - "烧杯", - "3a14196b-24f2-ca49-9081-0cab8021bf1a" - ], - "BIOYOND_PolymerStation_1BottleCarrier": [ - "试剂瓶", - "3a14196b-8bcf-a460-4f74-23f21ca79e72" - ], - "BIOYOND_PolymerStation_6StockCarrier": [ - "分装板", - "3a14196e-5dfe-6e21-0c79-fe2036d052c4" - ], - "BIOYOND_PolymerStation_Liquid_Vial": [ - "10%分装小瓶", - "3a14196c-76be-2279-4e22-7310d69aed68" - ], - "BIOYOND_PolymerStation_Solid_Vial": [ - "90%分装小瓶", - "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea" - ], - "BIOYOND_PolymerStation_8StockCarrier": [ - "样品板", - "3a14196e-b7a0-a5da-1931-35f3000281e9" - ], - "BIOYOND_PolymerStation_Solid_Stock": [ - "样品瓶", - "3a14196a-cf7d-8aea-48d8-b9662c7dba94" - ] + "api_key": "YOUR_API_KEY", + "api_host": "http://your-api-host:port", + "material_type_mappings": { + "BIOYOND_PolymerStation_1FlaskCarrier": [ + "烧杯", + "uuid-placeholder-flask" + ], + "BIOYOND_PolymerStation_1BottleCarrier": [ + "试剂瓶", + "uuid-placeholder-bottle" + ], + "BIOYOND_PolymerStation_6StockCarrier": [ + "分装板", + "uuid-placeholder-stock-6" + ], + "BIOYOND_PolymerStation_Liquid_Vial": [ + "10%分装小瓶", + "uuid-placeholder-liquid-vial" + ], + "BIOYOND_PolymerStation_Solid_Vial": [ + "90%分装小瓶", + "uuid-placeholder-solid-vial" + ], + "BIOYOND_PolymerStation_8StockCarrier": [ + "样品板", + "uuid-placeholder-stock-8" + ], + "BIOYOND_PolymerStation_Solid_Stock": [ + "样品瓶", + "uuid-placeholder-solid-stock" + ] + }, + "warehouse_mapping": { + "粉末堆栈": { + "uuid": "uuid-placeholder-powder-stack", + "site_uuids": { + "A01": "uuid-placeholder-powder-A01", + "A02": "uuid-placeholder-powder-A02", + "A03": "uuid-placeholder-powder-A03", + "A04": "uuid-placeholder-powder-A04", + "B01": "uuid-placeholder-powder-B01", + "B02": "uuid-placeholder-powder-B02", + "B03": "uuid-placeholder-powder-B03", + "B04": "uuid-placeholder-powder-B04", + "C01": "uuid-placeholder-powder-C01", + "C02": "uuid-placeholder-powder-C02", + "C03": "uuid-placeholder-powder-C03", + "C04": "uuid-placeholder-powder-C04", + "D01": "uuid-placeholder-powder-D01", + "D02": "uuid-placeholder-powder-D02", + "D03": "uuid-placeholder-powder-D03", + "D04": "uuid-placeholder-powder-D04" + } + }, + "溶液堆栈": { + "uuid": "uuid-placeholder-liquid-stack", + "site_uuids": { + "A01": "uuid-placeholder-liquid-A01", + "A02": "uuid-placeholder-liquid-A02", + "A03": "uuid-placeholder-liquid-A03", + "A04": "uuid-placeholder-liquid-A04", + "B01": "uuid-placeholder-liquid-B01", + "B02": "uuid-placeholder-liquid-B02", + "B03": "uuid-placeholder-liquid-B03", + "B04": "uuid-placeholder-liquid-B04", + "C01": "uuid-placeholder-liquid-C01", + "C02": "uuid-placeholder-liquid-C02", + "C03": "uuid-placeholder-liquid-C03", + "C04": "uuid-placeholder-liquid-C04", + "D01": "uuid-placeholder-liquid-D01", + "D02": "uuid-placeholder-liquid-D02", + "D03": "uuid-placeholder-liquid-D03", + "D04": "uuid-placeholder-liquid-D04" + } + }, + "试剂堆栈": { + "uuid": "uuid-placeholder-reagent-stack", + "site_uuids": { + "A01": "uuid-placeholder-reagent-A01", + "A02": "uuid-placeholder-reagent-A02", + "A03": "uuid-placeholder-reagent-A03", + "A04": "uuid-placeholder-reagent-A04", + "B01": "uuid-placeholder-reagent-B01", + "B02": "uuid-placeholder-reagent-B02", + "B03": "uuid-placeholder-reagent-B03", + "B04": "uuid-placeholder-reagent-B04" + } } }, - "deck": { - "data": { - "_resource_child_name": "Bioyond_Dispensing_Deck", - "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck" + "http_service_config": { + "http_service_host": "127.0.0.1", + "http_service_port": 8080 + }, + "material_default_parameters": { + "NMP": { + "unit": "毫升", + "density": "1.03", + "densityUnit": "g/mL", + "description": "N-甲基吡咯烷酮 (N-Methyl-2-pyrrolidone)" } }, - "protocol_type": [] + "material_type_parameters": {} + }, + "deck": { + "data": { + "_resource_child_name": "Bioyond_Dispensing_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck" + } }, + "size_x": 2700.0, + "size_y": 1080.0, + "size_z": 1500.0, + "protocol_type": [], "data": {} }, { @@ -80,4 +156,4 @@ "data": {} } ] -} +} \ No newline at end of file diff --git a/unilabos/test/experiments/reaction_station_bioyond.json b/unilabos/test/experiments/reaction_station_bioyond.json index 5cbe5b43..74f2d6d9 100644 --- a/unilabos/test/experiments/reaction_station_bioyond.json +++ b/unilabos/test/experiments/reaction_station_bioyond.json @@ -14,60 +14,200 @@ ], "type": "device", "class": "reaction_station.bioyond", - "position": {"x": 0, "y": 3800, "z": 0}, + "position": { + "x": 0, + "y": 1100, + "z": 0 + }, "config": { - "config": { - "api_key": "DE9BDDA0", - "api_host": "http://192.168.1.200:44402", - "workflow_mappings": { - "reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1", - "reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6", - "Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6", - "Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47", - "Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046", - "Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6", - "liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", - "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" + "api_key": "YOUR_API_KEY", + "api_host": "http://your-api-host:port", + "workflow_mappings": { + "reactor_taken_out": "workflow-uuid-reactor-out", + "reactor_taken_in": "workflow-uuid-reactor-in", + "Solid_feeding_vials": "workflow-uuid-solid-vials", + "Liquid_feeding_vials(non-titration)": "workflow-uuid-liquid-vials", + "Liquid_feeding_solvents": "workflow-uuid-solvents", + "Liquid_feeding(titration)": "workflow-uuid-titration", + "liquid_feeding_beaker": "workflow-uuid-beaker", + "Drip_back": "workflow-uuid-drip-back" + }, + "material_type_mappings": { + "BIOYOND_PolymerStation_Reactor": [ + "反应器", + "uuid-placeholder-reactor" + ], + "BIOYOND_PolymerStation_1BottleCarrier": [ + "试剂瓶", + "uuid-placeholder-bottle" + ], + "BIOYOND_PolymerStation_1FlaskCarrier": [ + "烧杯", + "uuid-placeholder-beaker" + ], + "BIOYOND_PolymerStation_6StockCarrier": [ + "样品板", + "uuid-placeholder-sample-plate" + ], + "BIOYOND_PolymerStation_Solid_Vial": [ + "90%分装小瓶", + "uuid-placeholder-solid-vial" + ], + "BIOYOND_PolymerStation_Liquid_Vial": [ + "10%分装小瓶", + "uuid-placeholder-liquid-vial" + ], + "BIOYOND_PolymerStation_TipBox": [ + "枪头盒", + "uuid-placeholder-tipbox" + ], + "BIOYOND_PolymerStation_Measurement_Vial": [ + "测量小瓶", + "uuid-placeholder-measure-vial" + ] + }, + "warehouse_mapping": { + "堆栈1左": { + "uuid": "uuid-placeholder-stack1-left", + "site_uuids": { + "A01": "uuid-placeholder-site-A01", + "A02": "uuid-placeholder-site-A02", + "A03": "uuid-placeholder-site-A03", + "A04": "uuid-placeholder-site-A04", + "B01": "uuid-placeholder-site-B01", + "B02": "uuid-placeholder-site-B02", + "B03": "uuid-placeholder-site-B03", + "B04": "uuid-placeholder-site-B04", + "C01": "uuid-placeholder-site-C01", + "C02": "uuid-placeholder-site-C02", + "C03": "uuid-placeholder-site-C03", + "C04": "uuid-placeholder-site-C04", + "D01": "uuid-placeholder-site-D01", + "D02": "uuid-placeholder-site-D02", + "D03": "uuid-placeholder-site-D03", + "D04": "uuid-placeholder-site-D04" + } + }, + "堆栈1右": { + "uuid": "uuid-placeholder-stack1-right", + "site_uuids": { + "A05": "uuid-placeholder-site-A05", + "A06": "uuid-placeholder-site-A06", + "A07": "uuid-placeholder-site-A07", + "A08": "uuid-placeholder-site-A08", + "B05": "uuid-placeholder-site-B05", + "B06": "uuid-placeholder-site-B06", + "B07": "uuid-placeholder-site-B07", + "B08": "uuid-placeholder-site-B08", + "C05": "uuid-placeholder-site-C05", + "C06": "uuid-placeholder-site-C06", + "C07": "uuid-placeholder-site-C07", + "C08": "uuid-placeholder-site-C08", + "D05": "uuid-placeholder-site-D05", + "D06": "uuid-placeholder-site-D06", + "D07": "uuid-placeholder-site-D07", + "D08": "uuid-placeholder-site-D08" + } + }, + "站内试剂存放堆栈": { + "uuid": "uuid-placeholder-reagent-stack", + "site_uuids": { + "A01": "uuid-placeholder-reagent-A01", + "A02": "uuid-placeholder-reagent-A02" + } + }, + "测量小瓶仓库(测密度)": { + "uuid": "uuid-placeholder-density-stack", + "site_uuids": { + "A01": "uuid-placeholder-density-A01", + "A02": "uuid-placeholder-density-A02", + "A03": "uuid-placeholder-density-A03", + "B01": "uuid-placeholder-density-B01", + "B02": "uuid-placeholder-density-B02", + "B03": "uuid-placeholder-density-B03" + } + }, + "站内Tip盒堆栈(左)": { + "uuid": "uuid-placeholder-tipstack-left", + "site_uuids": { + "A02": "uuid-placeholder-tip-A02", + "A03": "uuid-placeholder-tip-A03", + "B02": "uuid-placeholder-tip-B02", + "B03": "uuid-placeholder-tip-B03" + } }, - "material_type_mappings": { - "BIOYOND_PolymerStation_Reactor": [ - "反应器", - "3a14233b-902d-0d7b-4533-3f60f1c41c1b" - ], - "BIOYOND_PolymerStation_1BottleCarrier": [ - "试剂瓶", - "3a14233b-56e3-6c53-a8ab-fcaac163a9ba" - ], - "BIOYOND_PolymerStation_1FlaskCarrier": [ - "烧杯", - "3a14233b-f0a9-ba84-eaa9-0d4718b361b6" - ], - "BIOYOND_PolymerStation_6StockCarrier": [ - "样品板", - "3a142339-80de-8f25-6093-1b1b1b6c322e" - ], - "BIOYOND_PolymerStation_Solid_Vial": [ - "90%分装小瓶", - "3a14233a-26e1-28f8-af6a-60ca06ba0165" - ], - "BIOYOND_PolymerStation_Liquid_Vial": [ - "10%分装小瓶", - "3a14233a-84a3-088d-6676-7cb4acd57c64" - ], - "BIOYOND_PolymerStation_TipBox": [ - "枪头盒", - "3a143890-9d51-60ac-6d6f-6edb43c12041" - ] + "站内Tip盒堆栈(右)": { + "uuid": "uuid-placeholder-tipstack-right", + "site_uuids": { + "A01": "uuid-placeholder-tip-A01", + "B01": "uuid-placeholder-tip-B01" + } } }, - "deck": { - "data": { - "_resource_child_name": "Bioyond_Deck", - "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" + "workflow_to_section_map": { + "reactor_taken_in": "反应器放入", + "reactor_taken_out": "反应器取出", + "Solid_feeding_vials": "固体投料-小瓶", + "Liquid_feeding_vials(non-titration)": "液体投料-小瓶(非滴定)", + "Liquid_feeding_solvents": "液体投料-溶剂", + "Liquid_feeding(titration)": "液体投料-滴定", + "liquid_feeding_beaker": "液体投料-烧杯", + "Drip_back": "液体回滴" + }, + "action_names": { + "reactor_taken_in": { + "config": "通量-配置", + "stirring": "反应模块-开始搅拌" + }, + "solid_feeding_vials": { + "feeding": "粉末加样模块-投料", + "observe": "反应模块-观察搅拌结果" + }, + "liquid_feeding_vials_non_titration": { + "liquid": "稀释液瓶加液位-液体投料", + "observe": "反应模块-滴定结果观察" + }, + "liquid_feeding_solvents": { + "liquid": "试剂AB放置位-试剂吸液分液", + "observe": "反应模块-观察搅拌结果" + }, + "liquid_feeding_titration": { + "liquid": "稀释液瓶加液位-稀释液吸液分液", + "observe": "反应模块-滴定结果观察" + }, + "liquid_feeding_beaker": { + "liquid": "烧杯溶液放置位-烧杯吸液分液", + "observe": "反应模块-观察搅拌结果" + }, + "drip_back": { + "liquid": "试剂AB放置位-试剂吸液分液", + "observe": "反应模块-向下滴定结果观察" } }, - "protocol_type": [] + "http_service_config": { + "http_service_host": "127.0.0.1", + "http_service_port": 8080 + }, + "material_default_parameters": { + "NMP": { + "unit": "毫升", + "density": "1.03", + "densityUnit": "g/mL", + "description": "N-甲基吡咯烷酮 (N-Methyl-2-pyrrolidone)" + } + }, + "material_type_parameters": {} + }, + "deck": { + "data": { + "_resource_child_name": "Bioyond_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" + } }, + "size_x": 2700.0, + "size_y": 1080.0, + "size_z": 2500.0, + "protocol_type": [], "data": {} }, { @@ -77,7 +217,11 @@ "parent": "reaction_station_bioyond", "type": "device", "class": "reaction_station.reactor", - "position": {"x": 1150, "y": 380, "z": 0}, + "position": { + "x": 1150, + "y": 300, + "z": 0 + }, "config": {}, "data": {} }, @@ -88,7 +232,11 @@ "parent": "reaction_station_bioyond", "type": "device", "class": "reaction_station.reactor", - "position": {"x": 1365, "y": 380, "z": 0}, + "position": { + "x": 1365, + "y": 300, + "z": 0 + }, "config": {}, "data": {} }, @@ -99,7 +247,11 @@ "parent": "reaction_station_bioyond", "type": "device", "class": "reaction_station.reactor", - "position": {"x": 1580, "y": 380, "z": 0}, + "position": { + "x": 1580, + "y": 300, + "z": 0 + }, "config": {}, "data": {} }, @@ -110,7 +262,11 @@ "parent": "reaction_station_bioyond", "type": "device", "class": "reaction_station.reactor", - "position": {"x": 1790, "y": 380, "z": 0}, + "position": { + "x": 1790, + "y": 300, + "z": 0 + }, "config": {}, "data": {} }, @@ -121,7 +277,11 @@ "parent": "reaction_station_bioyond", "type": "device", "class": "reaction_station.reactor", - "position": {"x": 2010, "y": 380, "z": 0}, + "position": { + "x": 2010, + "y": 300, + "z": 0 + }, "config": {}, "data": {} }, @@ -134,7 +294,7 @@ "class": "BIOYOND_PolymerReactionStation_Deck", "position": { "x": 0, - "y": 0, + "y": 1100, "z": 0 }, "config": { @@ -150,4 +310,4 @@ "data": {} } ] -} +} \ No newline at end of file diff --git a/unilabos/utils/README_LOGGING.md b/unilabos/utils/README_LOGGING.md new file mode 100644 index 00000000..9cb551bb --- /dev/null +++ b/unilabos/utils/README_LOGGING.md @@ -0,0 +1,187 @@ +# UniLabOS 日志配置说明 + +> **文件位置**: `unilabos/utils/log.py` +> **最后更新**: 2026-01-11 +> **维护者**: Uni-Lab-OS 开发团队 + +本文档说明 UniLabOS 日志系统中对第三方库和内部模块的日志级别配置,避免控制台被过多的 DEBUG 日志淹没。 + +--- + +## 📋 已屏蔽的日志 + +以下库/模块的日志已被设置为 **WARNING** 或 **INFO** 级别,不再显示 DEBUG 日志: + +### 1. pymodbus(Modbus 通信库) + +**配置位置**: `log.py` 第196-200行 + +```python +# pymodbus 库的日志太详细,设置为 WARNING +logging.getLogger('pymodbus').setLevel(logging.WARNING) +logging.getLogger('pymodbus.logging').setLevel(logging.WARNING) +logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING) +logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING) +``` + +**屏蔽原因**: +- pymodbus 在 DEBUG 级别会输出每一次 Modbus 通信的详细信息 +- 包括 `Processing: 0x5 0x1e 0x0 0x0...` 等原始数据 +- 包括 `decoded PDU function_code(3 sub -1) -> ReadHoldingRegistersResponse(...)` 等解码信息 +- 这些信息对日常使用价值不大,但会快速刷屏 + +**典型被屏蔽的日志**: +``` +[DEBUG] Processing: 0x5 0x1e 0x0 0x0 0x0 0x7 0x1 0x3 0x4 0x0 0x0 0x0 0x0 [handleFrame:72] [pymodbus.logging.base] +[DEBUG] decoded PDU function_code(3 sub -1) -> ReadHoldingRegistersResponse(...) [decode:79] [pymodbus.logging.decoders] +``` + +--- + +### 2. websockets(WebSocket 库) + +**配置位置**: `log.py` 第202-205行 + +```python +# websockets 库的日志输出较多,设置为 WARNING +logging.getLogger('websockets').setLevel(logging.WARNING) +logging.getLogger('websockets.client').setLevel(logging.WARNING) +logging.getLogger('websockets.server').setLevel(logging.WARNING) +``` + +**屏蔽原因**: +- WebSocket 连接、断开、心跳等信息在 DEBUG 级别会频繁输出 +- 对于长时间运行的服务,这些日志意义不大 + +--- + +### 3. ROS Host Node(设备状态更新) + +**配置位置**: `log.py` 第207-208行 + +```python +# ROS 节点的状态更新日志过于频繁,设置为 INFO +logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.INFO) +``` + +**屏蔽原因**: +- 设备状态更新(如手套箱压力)每隔几秒就会更新一次 +- DEBUG 日志会记录每一次状态变化,导致日志刷屏 +- 这些频繁的状态更新对调试价值不大 + +**典型被屏蔽的日志**: +``` +[DEBUG] [/devices/host_node] Status updated: BatteryStation.data_glove_box_pressure = 4.229457855224609 [property_callback:666] [unilabos.ros.nodes.presets.host_node] +``` + +--- + +### 4. asyncio 和 urllib3 + +**配置位置**: `log.py` 第224-225行 + +```python +logging.getLogger("asyncio").setLevel(logging.INFO) +logging.getLogger("urllib3").setLevel(logging.INFO) +``` + +**屏蔽原因**: +- asyncio: 异步 IO 的内部调试信息 +- urllib3: HTTP 请求库的连接池、重试等详细信息 + +--- + +## 🔧 如何临时启用这些日志(调试用) + +### 方法1: 修改 log.py(永久启用) + +在 `log.py` 的 `configure_logger()` 函数中,将对应库的日志级别改为 `logging.DEBUG`: + +```python +# 临时启用 pymodbus 的 DEBUG 日志 +logging.getLogger('pymodbus').setLevel(logging.DEBUG) +logging.getLogger('pymodbus.logging').setLevel(logging.DEBUG) +logging.getLogger('pymodbus.logging.base').setLevel(logging.DEBUG) +logging.getLogger('pymodbus.logging.decoders').setLevel(logging.DEBUG) +``` + +### 方法2: 在代码中临时启用(单次调试) + +在需要调试的代码文件中添加: + +```python +import logging + +# 临时启用 pymodbus DEBUG 日志 +logging.getLogger('pymodbus').setLevel(logging.DEBUG) + +# 你的 Modbus 调试代码 +... + +# 调试完成后恢复 +logging.getLogger('pymodbus').setLevel(logging.WARNING) +``` + +### 方法3: 使用环境变量或配置文件(推荐) + +未来可以考虑在启动参数中添加 `--debug-modbus` 等选项来动态控制。 + +--- + +## 📊 日志级别说明 + +| 级别 | 数值 | 用途 | 是否显示 | +|------|------|------|---------| +| TRACE | 5 | 最详细的跟踪信息 | ✅ | +| DEBUG | 10 | 调试信息 | ✅ | +| INFO | 20 | 一般信息 | ✅ | +| WARNING | 30 | 警告信息 | ✅ | +| ERROR | 40 | 错误信息 | ✅ | +| CRITICAL | 50 | 严重错误 | ✅ | + +**当前配置**: +- UniLabOS 自身代码: DEBUG 及以上全部显示 +- pymodbus/websockets: **WARNING** 及以上显示(屏蔽 DEBUG/INFO) +- ROS host_node: **INFO** 及以上显示(屏蔽 DEBUG) + +--- + +## ⚠️ 重要提示 + +### 修改生效时间 +- 修改 `log.py` 后需要 **重启 unilab 服务** 才能生效 +- 不需要重新安装或重新编译 + +### 调试 Modbus 通信问题 +如果需要调试 Modbus 通信故障,应该: +1. 临时启用 pymodbus DEBUG 日志(方法2) +2. 复现问题 +3. 查看详细的通信日志 +4. 调试完成后记得恢复 WARNING 级别 + +### 调试设备状态问题 +如果需要调试设备状态更新问题: +```python +logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.DEBUG) +``` + +--- + +## 📝 维护记录 + +| 日期 | 修改内容 | 操作人 | +|------|---------|--------| +| 2026-01-11 | 初始创建,添加 pymodbus、websockets、ROS host_node 屏蔽 | - | +| 2026-01-07 | 添加 pymodbus 和 websockets 屏蔽(log-0107.py) | - | + +--- + +## 🔗 相关文件 + +- `log.py` - 日志配置主文件 +- `unilabos/devices/workstation/coin_cell_assembly/` - 使用 Modbus 的扣电工作站代码 +- `unilabos/ros/nodes/presets/host_node.py` - ROS 主机节点代码 + +--- + +**维护提示**: 如果添加了新的第三方库或发现新的日志刷屏问题,请在此文档中记录并更新 `log.py` 配置。 diff --git a/unilabos/utils/log.py b/unilabos/utils/log.py index ffe13c02..f10bd518 100644 --- a/unilabos/utils/log.py +++ b/unilabos/utils/log.py @@ -191,6 +191,21 @@ def configure_logger(loglevel=None, working_dir=None): # 添加处理器到根日志记录器 root_logger.addHandler(console_handler) + + # 降低第三方库的日志级别,避免过多输出 + # pymodbus 库的日志太详细,设置为 WARNING + logging.getLogger('pymodbus').setLevel(logging.WARNING) + logging.getLogger('pymodbus.logging').setLevel(logging.WARNING) + logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING) + logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING) + + # websockets 库的日志输出较多,设置为 WARNING + logging.getLogger('websockets').setLevel(logging.WARNING) + logging.getLogger('websockets.client').setLevel(logging.WARNING) + logging.getLogger('websockets.server').setLevel(logging.WARNING) + + # ROS 节点的状态更新日志过于频繁,设置为 INFO + logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.INFO) # 如果指定了工作目录,添加文件处理器 if working_dir is not None: @@ -215,6 +230,7 @@ def configure_logger(loglevel=None, working_dir=None): logging.getLogger("urllib3").setLevel(logging.INFO) + # 配置日志系统 configure_logger() diff --git a/yibin_electrolyte_config_example.json b/yibin_electrolyte_config_example.json new file mode 100644 index 00000000..d5efc357 --- /dev/null +++ b/yibin_electrolyte_config_example.json @@ -0,0 +1,126 @@ +{ + "nodes": [ + { + "id": "bioyond_cell_workstation", + "name": "配液分液工站 (示例)", + "parent": null, + "children": [ + "YB_Bioyond_Deck" + ], + "type": "device", + "class": "bioyond_cell", + "config": { + "deck": { + "data": { + "_resource_child_name": "YB_Bioyond_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck" + } + }, + "protocol_type": [], + "bioyond_config": { + "api_host": "http://YOUR_API_HOST:PORT", + "api_key": "YOUR_API_KEY", + "timeout": 30, + "report_token": "YOUR_REPORT_TOKEN", + "HTTP_host": "YOUR_LOCAL_IP", + "HTTP_port": 8080, + "debug_mode": false, + "material_type_mappings": { + "100ml液体": [ + "YB_100ml_yeti", + "00000000-0000-0000-0000-000000000000" + ], + "液": [ + "YB_ye", + "00000000-0000-0000-0000-000000000000" + ], + "高粘液": [ + "YB_gaonianye", + "00000000-0000-0000-0000-000000000000" + ], + "加样头(大)": [ + "YB_jia_yang_tou_da_Carrier", + "00000000-0000-0000-0000-000000000000" + ], + "5ml分液瓶板": [ + "YB_5ml_fenyepingban", + "00000000-0000-0000-0000-000000000000" + ], + "5ml分液瓶": [ + "YB_5ml_fenyeping", + "00000000-0000-0000-0000-000000000000" + ], + "20ml分液瓶板": [ + "YB_20ml_fenyepingban", + "00000000-0000-0000-0000-000000000000" + ], + "20ml分液瓶": [ + "YB_20ml_fenyeping", + "00000000-0000-0000-0000-000000000000" + ], + "配液瓶(小)板": [ + "YB_peiyepingxiaoban", + "00000000-0000-0000-0000-000000000000" + ], + "配液瓶(小)": [ + "YB_pei_ye_xiao_Bottle", + "00000000-0000-0000-0000-000000000000" + ], + "枪头盒": [ + "YB_qiang_tou_he", + "00000000-0000-0000-0000-000000000000" + ] + }, + "warehouse_mapping": { + "示例堆栈": { + "uuid": "00000000-0000-0000-0000-000000000000", + "site_uuids": { + "A01": "00000000-0000-0000-0000-000000000000", + "B01": "00000000-0000-0000-0000-000000000000" + } + } + }, + "solid_liquid_mappings": { + "示例物料": { + "typeId": "00000000-0000-0000-0000-000000000000", + "code": "", + "barCode": "", + "name": "Example_Material", + "unit": "g", + "parameters": "", + "quantity": "2", + "warningQuantity": "1", + "details": [] + } + } + } + }, + "data": {} + }, + { + "id": "YB_Bioyond_Deck", + "name": "YB_Bioyond_Deck", + "children": [], + "parent": "bioyond_cell_workstation", + "type": "deck", + "class": "BIOYOND_YB_Deck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "BIOYOND_YB_Deck", + "setup": true, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + } + ], + "links": [] +} \ No newline at end of file