mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-05 22:15:04 +00:00
Merge branch 'prcix9320' into prcxi9320
This commit is contained in:
@@ -20,6 +20,7 @@ if unilabos_dir not in sys.path:
|
||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
||||
|
||||
|
||||
def load_config_from_file(config_path):
|
||||
if config_path is None:
|
||||
config_path = os.environ.get("UNILABOS_BASICCONFIG_CONFIG_PATH", None)
|
||||
@@ -41,7 +42,7 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
|
||||
for i, arg in enumerate(sys.argv):
|
||||
for option_string in option_strings:
|
||||
if arg.startswith(option_string):
|
||||
new_arg = arg[:2] + arg[2:len(option_string)].replace("-", "_") + arg[len(option_string):]
|
||||
new_arg = arg[:2] + arg[2 : len(option_string)].replace("-", "_") + arg[len(option_string) :]
|
||||
sys.argv[i] = new_arg
|
||||
break
|
||||
|
||||
@@ -49,6 +50,8 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
|
||||
subparsers = parser.add_subparsers(title="Valid subcommands", dest="command")
|
||||
|
||||
parser.add_argument("-g", "--graph", help="Physical setup graph file path.")
|
||||
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file path.")
|
||||
parser.add_argument(
|
||||
@@ -153,21 +156,54 @@ def parse_args():
|
||||
default=False,
|
||||
help="Complete registry information",
|
||||
)
|
||||
# workflow upload subcommand
|
||||
workflow_parser = subparsers.add_parser(
|
||||
"workflow_upload",
|
||||
aliases=["wf"],
|
||||
help="Upload workflow from xdl/json/python files",
|
||||
)
|
||||
workflow_parser.add_argument(
|
||||
"-f",
|
||||
"--workflow_file",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to the workflow file (JSON format)",
|
||||
)
|
||||
workflow_parser.add_argument(
|
||||
"-n",
|
||||
"--workflow_name",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Workflow name, if not provided will use the name from file or filename",
|
||||
)
|
||||
workflow_parser.add_argument(
|
||||
"--tags",
|
||||
type=str,
|
||||
nargs="*",
|
||||
default=[],
|
||||
help="Tags for the workflow (space-separated)",
|
||||
)
|
||||
workflow_parser.add_argument(
|
||||
"--published",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Whether to publish the workflow (default: False)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 解析命令行参数
|
||||
args = parse_args()
|
||||
convert_argv_dashes_to_underscores(args)
|
||||
args_dict = vars(args.parse_args())
|
||||
parser = parse_args()
|
||||
convert_argv_dashes_to_underscores(parser)
|
||||
args = parser.parse_args()
|
||||
args_dict = vars(args)
|
||||
|
||||
# 环境检查 - 检查并自动安装必需的包 (可选)
|
||||
if not args_dict.get("skip_env_check", False):
|
||||
from unilabos.utils.environment_check import check_environment
|
||||
|
||||
print_status("正在进行环境依赖检查...", "info")
|
||||
if not check_environment(auto_install=True):
|
||||
print_status("环境检查失败,程序退出", "error")
|
||||
os._exit(1)
|
||||
@@ -220,17 +256,18 @@ def main():
|
||||
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
||||
configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
||||
|
||||
if args_dict["addr"] == "test":
|
||||
print_status("使用测试环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
elif args_dict["addr"] == "uat":
|
||||
print_status("使用uat环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.uat.bohrium.com/api/v1"
|
||||
elif args_dict["addr"] == "local":
|
||||
print_status("使用本地环境地址", "info")
|
||||
HTTPConfig.remote_addr = "http://127.0.0.1:48197/api/v1"
|
||||
else:
|
||||
HTTPConfig.remote_addr = args_dict.get("addr", "")
|
||||
if args.addr != parser.get_default("addr"):
|
||||
if args.addr == "test":
|
||||
print_status("使用测试环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
elif args.addr == "uat":
|
||||
print_status("使用uat环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.uat.bohrium.com/api/v1"
|
||||
elif args.addr == "local":
|
||||
print_status("使用本地环境地址", "info")
|
||||
HTTPConfig.remote_addr = "http://127.0.0.1:48197/api/v1"
|
||||
else:
|
||||
HTTPConfig.remote_addr = args.addr
|
||||
|
||||
# 设置BasicConfig参数
|
||||
if args_dict.get("ak", ""):
|
||||
@@ -239,9 +276,12 @@ def main():
|
||||
if args_dict.get("sk", ""):
|
||||
BasicConfig.sk = args_dict.get("sk", "")
|
||||
print_status("传入了sk参数,优先采用传入参数!", "info")
|
||||
BasicConfig.working_dir = working_dir
|
||||
|
||||
workflow_upload = args_dict.get("command") in ("workflow_upload", "wf")
|
||||
|
||||
# 使用远程资源启动
|
||||
if args_dict["use_remote_resource"]:
|
||||
if not workflow_upload and args_dict["use_remote_resource"]:
|
||||
print_status("使用远程资源启动", "info")
|
||||
from unilabos.app.web import http_client
|
||||
|
||||
@@ -254,7 +294,6 @@ def main():
|
||||
|
||||
BasicConfig.port = args_dict["port"] if args_dict["port"] else BasicConfig.port
|
||||
BasicConfig.disable_browser = args_dict["disable_browser"] or BasicConfig.disable_browser
|
||||
BasicConfig.working_dir = working_dir
|
||||
BasicConfig.is_host_mode = not args_dict.get("is_slave", False)
|
||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||
@@ -283,9 +322,31 @@ def main():
|
||||
|
||||
# 注册表
|
||||
lab_registry = build_registry(
|
||||
args_dict["registry_path"], args_dict.get("complete_registry", False), args_dict["upload_registry"]
|
||||
args_dict["registry_path"], args_dict.get("complete_registry", False), BasicConfig.upload_registry
|
||||
)
|
||||
|
||||
if BasicConfig.upload_registry:
|
||||
# 设备注册到服务端 - 需要 ak 和 sk
|
||||
if BasicConfig.ak and BasicConfig.sk:
|
||||
print_status("开始注册设备到服务端...", "info")
|
||||
try:
|
||||
register_devices_and_resources(lab_registry)
|
||||
print_status("设备注册完成", "info")
|
||||
except Exception as e:
|
||||
print_status(f"设备注册失败: {e}", "error")
|
||||
else:
|
||||
print_status("未提供 ak 和 sk,跳过设备注册", "info")
|
||||
else:
|
||||
print_status("本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning")
|
||||
|
||||
# 处理 workflow_upload 子命令
|
||||
if workflow_upload:
|
||||
from unilabos.workflow.wf_utils import handle_workflow_upload_command
|
||||
|
||||
handle_workflow_upload_command(args_dict)
|
||||
print_status("工作流上传完成,程序退出", "info")
|
||||
os._exit(0)
|
||||
|
||||
if not BasicConfig.ak or not BasicConfig.sk:
|
||||
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
|
||||
os._exit(1)
|
||||
@@ -327,6 +388,10 @@ def main():
|
||||
for ind, i in enumerate(resource_edge_info[::-1]):
|
||||
source_node: ResourceDict = nodes[i["source"]]
|
||||
target_node: ResourceDict = nodes[i["target"]]
|
||||
if "sourceHandle" not in source_node:
|
||||
continue
|
||||
if "targetHandle" not in target_node:
|
||||
continue
|
||||
source_handle = i["sourceHandle"]
|
||||
target_handle = i["targetHandle"]
|
||||
source_handler_keys = [
|
||||
@@ -362,20 +427,6 @@ def main():
|
||||
args_dict["devices_config"] = resource_tree_set
|
||||
args_dict["graph"] = graph_res.physical_setup_graph
|
||||
|
||||
if BasicConfig.upload_registry:
|
||||
# 设备注册到服务端 - 需要 ak 和 sk
|
||||
if BasicConfig.ak and BasicConfig.sk:
|
||||
print_status("开始注册设备到服务端...", "info")
|
||||
try:
|
||||
register_devices_and_resources(lab_registry)
|
||||
print_status("设备注册完成", "info")
|
||||
except Exception as e:
|
||||
print_status(f"设备注册失败: {e}", "error")
|
||||
else:
|
||||
print_status("未提供 ak 和 sk,跳过设备注册", "info")
|
||||
else:
|
||||
print_status("本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning")
|
||||
|
||||
if args_dict["controllers"] is not None:
|
||||
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
|
||||
else:
|
||||
@@ -390,6 +441,7 @@ def main():
|
||||
comm_client = get_communication_client()
|
||||
if "websocket" in args_dict["app_bridges"]:
|
||||
args_dict["bridges"].append(comm_client)
|
||||
|
||||
def _exit(signum, frame):
|
||||
comm_client.stop()
|
||||
sys.exit(0)
|
||||
@@ -431,16 +483,13 @@ def main():
|
||||
resource_visualization.start()
|
||||
except OSError as e:
|
||||
if "AMENT_PREFIX_PATH" in str(e):
|
||||
print_status(
|
||||
f"ROS 2环境未正确设置,跳过3D可视化启动。错误详情: {e}",
|
||||
"warning"
|
||||
)
|
||||
print_status(f"ROS 2环境未正确设置,跳过3D可视化启动。错误详情: {e}", "warning")
|
||||
print_status(
|
||||
"建议解决方案:\n"
|
||||
"1. 激活Conda环境: conda activate unilab\n"
|
||||
"2. 或使用 --backend simple 参数\n"
|
||||
"3. 或使用 --visual disable 参数禁用可视化",
|
||||
"info"
|
||||
"info",
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -76,7 +76,8 @@ class HTTPClient:
|
||||
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
||||
"""
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps({"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, indent=4))
|
||||
payload = {"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}
|
||||
f.write(json.dumps(payload, indent=4))
|
||||
# 从序列化数据中提取所有节点的UUID(保存旧UUID)
|
||||
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
||||
if not self.initialized or first_add:
|
||||
@@ -299,6 +300,10 @@ class HTTPClient:
|
||||
)
|
||||
if response.status_code not in [200, 201]:
|
||||
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"注册资源失败: {response.text}")
|
||||
return response
|
||||
|
||||
def request_startup_json(self) -> Optional[Dict[str, Any]]:
|
||||
@@ -331,6 +336,67 @@ class HTTPClient:
|
||||
logger.error(f"响应内容: {response.text}")
|
||||
return None
|
||||
|
||||
def workflow_import(
|
||||
self,
|
||||
name: str,
|
||||
workflow_uuid: str,
|
||||
workflow_name: str,
|
||||
nodes: List[Dict[str, Any]],
|
||||
edges: List[Dict[str, Any]],
|
||||
tags: Optional[List[str]] = None,
|
||||
published: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
导入工作流到服务器
|
||||
|
||||
Args:
|
||||
name: 工作流名称(顶层)
|
||||
workflow_uuid: 工作流UUID
|
||||
workflow_name: 工作流名称(data内部)
|
||||
nodes: 工作流节点列表
|
||||
edges: 工作流边列表
|
||||
tags: 工作流标签列表,默认为空列表
|
||||
published: 是否发布工作流,默认为False
|
||||
|
||||
Returns:
|
||||
Dict: API响应数据,包含 code 和 data (uuid, name)
|
||||
"""
|
||||
# target_lab_uuid 暂时使用默认值,后续由后端根据 ak/sk 获取
|
||||
payload = {
|
||||
"target_lab_uuid": "28c38bb0-63f6-4352-b0d8-b5b8eb1766d5",
|
||||
"name": name,
|
||||
"data": {
|
||||
"workflow_uuid": workflow_uuid,
|
||||
"workflow_name": workflow_name,
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"tags": tags if tags is not None else [],
|
||||
"published": published,
|
||||
},
|
||||
}
|
||||
# 保存请求到文件
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_workflow_upload.json"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(payload, indent=4, ensure_ascii=False))
|
||||
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/workflow/owner/import",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=60,
|
||||
)
|
||||
# 保存响应到文件
|
||||
with open(os.path.join(BasicConfig.working_dir, "res_workflow_upload.json"), "w", encoding="utf-8") as f:
|
||||
f.write(f"{response.status_code}" + "\n" + response.text)
|
||||
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"导入工作流失败: {response.text}")
|
||||
return res
|
||||
else:
|
||||
logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
|
||||
return {"code": response.status_code, "message": response.text}
|
||||
|
||||
|
||||
# 创建默认客户端实例
|
||||
http_client = HTTPClient()
|
||||
|
||||
@@ -438,7 +438,7 @@ class MessageProcessor:
|
||||
self.connected = True
|
||||
self.reconnect_count = 0
|
||||
|
||||
logger.info(f"[MessageProcessor] Connected to {self.websocket_url}")
|
||||
logger.trace(f"[MessageProcessor] Connected to {self.websocket_url}")
|
||||
|
||||
# 启动发送协程
|
||||
send_task = asyncio.create_task(self._send_handler())
|
||||
@@ -503,7 +503,7 @@ class MessageProcessor:
|
||||
|
||||
async def _send_handler(self):
|
||||
"""处理发送队列中的消息"""
|
||||
logger.debug("[MessageProcessor] Send handler started")
|
||||
logger.trace("[MessageProcessor] Send handler started")
|
||||
|
||||
try:
|
||||
while self.connected and self.websocket:
|
||||
@@ -965,7 +965,7 @@ class QueueProcessor:
|
||||
|
||||
def _run(self):
|
||||
"""运行队列处理主循环"""
|
||||
logger.debug("[QueueProcessor] Queue processor started")
|
||||
logger.trace("[QueueProcessor] Queue processor started")
|
||||
|
||||
while self.is_running:
|
||||
try:
|
||||
@@ -1175,7 +1175,6 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
else:
|
||||
url = f"{scheme}://{parsed.netloc}/api/v1/ws/schedule"
|
||||
|
||||
logger.debug(f"[WebSocketClient] URL: {url}")
|
||||
return url
|
||||
|
||||
def start(self) -> None:
|
||||
@@ -1188,13 +1187,11 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
logger.error("[WebSocketClient] WebSocket URL not configured")
|
||||
return
|
||||
|
||||
logger.info(f"[WebSocketClient] Starting connection to {self.websocket_url}")
|
||||
|
||||
# 启动两个核心线程
|
||||
self.message_processor.start()
|
||||
self.queue_processor.start()
|
||||
|
||||
logger.info("[WebSocketClient] All threads started")
|
||||
logger.trace("[WebSocketClient] All threads started")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止WebSocket客户端"""
|
||||
|
||||
@@ -21,7 +21,8 @@ class BasicConfig:
|
||||
startup_json_path = None # 填写绝对路径
|
||||
disable_browser = False # 禁止浏览器自动打开
|
||||
port = 8002 # 本地HTTP服务
|
||||
log_level: Literal['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = "DEBUG" # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
||||
|
||||
@classmethod
|
||||
def auth_secret(cls):
|
||||
@@ -41,7 +42,7 @@ class WSConfig:
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "http://127.0.0.1:48197/api/v1"
|
||||
remote_addr = "https://uni-lab.bohrium.com/api/v1"
|
||||
|
||||
|
||||
# ROS配置
|
||||
@@ -65,13 +66,14 @@ def _update_config_from_module(module):
|
||||
if not attr.startswith("_"):
|
||||
setattr(obj, attr, getattr(getattr(module, name), attr))
|
||||
|
||||
|
||||
def _update_config_from_env():
|
||||
prefix = "UNILABOS_"
|
||||
for env_key, env_value in os.environ.items():
|
||||
if not env_key.startswith(prefix):
|
||||
continue
|
||||
try:
|
||||
key_path = env_key[len(prefix):] # Remove UNILAB_ prefix
|
||||
key_path = env_key[len(prefix) :] # Remove UNILAB_ prefix
|
||||
class_field = key_path.upper().split("_", 1)
|
||||
if len(class_field) != 2:
|
||||
logger.warning(f"[ENV] 环境变量格式不正确:{env_key}")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Tuple, Union, Optional, Any, List
|
||||
|
||||
from opcua import Client, Node
|
||||
from opcua import Client, Node, ua
|
||||
from opcua.ua import NodeId, NodeClass, VariantType
|
||||
|
||||
|
||||
@@ -47,23 +47,68 @@ class Base(ABC):
|
||||
def _get_node(self) -> Node:
|
||||
if self._node is None:
|
||||
try:
|
||||
# 检查是否是NumericNodeId(ns=X;i=Y)格式
|
||||
if "NumericNodeId" in self._node_id:
|
||||
# 从字符串中提取命名空间和标识符
|
||||
import re
|
||||
match = re.search(r'ns=(\d+);i=(\d+)', self._node_id)
|
||||
if match:
|
||||
ns = int(match.group(1))
|
||||
identifier = int(match.group(2))
|
||||
node_id = NodeId(identifier, ns)
|
||||
self._node = self._client.get_node(node_id)
|
||||
# 尝试多种 NodeId 字符串格式解析,兼容不同服务器/库的输出
|
||||
# 可能的格式示例: 'ns=2;i=1234', 'ns=2;s=SomeString',
|
||||
# 'StringNodeId(ns=4;s=OPC|变量名)', 'NumericNodeId(ns=2;i=1234)' 等
|
||||
import re
|
||||
|
||||
nid = self._node_id
|
||||
# 如果已经是 NodeId/Node 对象(库用户可能传入),直接使用
|
||||
try:
|
||||
from opcua.ua import NodeId as UaNodeId
|
||||
if isinstance(nid, UaNodeId):
|
||||
self._node = self._client.get_node(nid)
|
||||
return self._node
|
||||
except Exception:
|
||||
# 若导入或类型判断失败,则继续下一步
|
||||
pass
|
||||
|
||||
# 直接以字符串形式处理
|
||||
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)
|
||||
if match_wrapped:
|
||||
# 提取括号内的实际 node_id 字符串
|
||||
nid = match_wrapped.group(2).strip()
|
||||
|
||||
# 常见短格式 'ns=2;i=1234' 或 'ns=2;s=SomeString'
|
||||
if re.match(r'^ns=\d+;[is]=', nid):
|
||||
self._node = self._client.get_node(nid)
|
||||
else:
|
||||
raise ValueError(f"无法解析节点ID: {self._node_id}")
|
||||
# 尝试提取 ns 和 i 或 s
|
||||
# 对于字符串标识符,可能包含特殊字符,使用非贪婪匹配
|
||||
m_num = re.search(r'ns=(\d+);i=(\d+)', nid)
|
||||
m_str = re.search(r'ns=(\d+);s=(.+?)(?:\)|$)', nid)
|
||||
if m_num:
|
||||
ns = int(m_num.group(1))
|
||||
identifier = int(m_num.group(2))
|
||||
node_id = NodeId(identifier, ns)
|
||||
self._node = self._client.get_node(node_id)
|
||||
elif m_str:
|
||||
ns = int(m_str.group(1))
|
||||
identifier = m_str.group(2).strip()
|
||||
# 对于字符串标识符,直接使用字符串格式
|
||||
node_id_str = f"ns={ns};s={identifier}"
|
||||
self._node = self._client.get_node(node_id_str)
|
||||
else:
|
||||
# 回退:尝试直接传入字符串(有些实现接受其它格式)
|
||||
try:
|
||||
self._node = self._client.get_node(self._node_id)
|
||||
except Exception as e:
|
||||
# 输出更详细的错误信息供调试
|
||||
print(f"获取节点失败(尝试直接字符串): {self._node_id}, 错误: {e}")
|
||||
raise
|
||||
else:
|
||||
# 直接使用节点ID字符串
|
||||
# 非字符串,尝试直接使用
|
||||
self._node = self._client.get_node(self._node_id)
|
||||
except Exception as e:
|
||||
print(f"获取节点失败: {self._node_id}, 错误: {e}")
|
||||
# 添加额外提示,帮助定位 BadNodeIdUnknown 问题
|
||||
print("提示: 请确认该 node_id 是否来自当前连接的服务器地址空间," \
|
||||
"以及 CSV/配置中名称与服务器 BrowseName 是否匹配。")
|
||||
raise
|
||||
return self._node
|
||||
|
||||
@@ -104,7 +149,56 @@ class Variable(Base):
|
||||
|
||||
def write(self, value: Any) -> bool:
|
||||
try:
|
||||
self._get_node().set_value(value)
|
||||
# 如果声明了数据类型,则尝试转换并使用对应的 Variant 写入
|
||||
coerced = value
|
||||
try:
|
||||
if self._data_type is not None:
|
||||
# 基于声明的数据类型做简单类型转换
|
||||
dt = self._data_type
|
||||
if dt in (DataType.SBYTE, DataType.BYTE, DataType.INT16, DataType.UINT16,
|
||||
DataType.INT32, DataType.UINT32, DataType.INT64, DataType.UINT64):
|
||||
# 数值类型 -> int
|
||||
if isinstance(value, str):
|
||||
coerced = int(value)
|
||||
else:
|
||||
coerced = int(value)
|
||||
elif dt in (DataType.FLOAT, DataType.DOUBLE):
|
||||
if isinstance(value, str):
|
||||
coerced = float(value)
|
||||
else:
|
||||
coerced = float(value)
|
||||
elif dt == DataType.BOOLEAN:
|
||||
if isinstance(value, str):
|
||||
v = value.strip().lower()
|
||||
if v in ("true", "1", "yes", "on"):
|
||||
coerced = True
|
||||
elif v in ("false", "0", "no", "off"):
|
||||
coerced = False
|
||||
else:
|
||||
coerced = bool(value)
|
||||
else:
|
||||
coerced = bool(value)
|
||||
elif dt == DataType.STRING or dt == DataType.BYTESTRING or dt == DataType.DATETIME:
|
||||
coerced = str(value)
|
||||
|
||||
# 使用 ua.Variant 明确指定 VariantType
|
||||
try:
|
||||
variant = ua.Variant(coerced, dt.value)
|
||||
self._get_node().set_value(variant)
|
||||
except Exception:
|
||||
# 回退:有些 set_value 实现接受 (value, variant_type)
|
||||
try:
|
||||
self._get_node().set_value(coerced, dt.value)
|
||||
except Exception:
|
||||
# 最后回退到直接写入(保持兼容性)
|
||||
self._get_node().set_value(coerced)
|
||||
else:
|
||||
# 未声明数据类型,直接写入
|
||||
self._get_node().set_value(value)
|
||||
except Exception:
|
||||
# 若在转换或按数据类型写入失败,尝试直接写入原始值并让上层捕获错误
|
||||
self._get_node().set_value(value)
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"写入变量 {self._name} 失败: {e}")
|
||||
@@ -120,20 +214,50 @@ class Method(Base):
|
||||
def _get_parent_node(self) -> Node:
|
||||
if self._parent_node is None:
|
||||
try:
|
||||
# 检查是否是NumericNodeId(ns=X;i=Y)格式
|
||||
if "NumericNodeId" in self._parent_node_id:
|
||||
# 从字符串中提取命名空间和标识符
|
||||
import re
|
||||
match = re.search(r'ns=(\d+);i=(\d+)', self._parent_node_id)
|
||||
if match:
|
||||
ns = int(match.group(1))
|
||||
identifier = int(match.group(2))
|
||||
node_id = NodeId(identifier, ns)
|
||||
self._parent_node = self._client.get_node(node_id)
|
||||
# 处理父节点ID,使用与_get_node相同的解析逻辑
|
||||
import re
|
||||
|
||||
nid = self._parent_node_id
|
||||
|
||||
# 如果已经是 NodeId 对象,直接使用
|
||||
try:
|
||||
from opcua.ua import NodeId as UaNodeId
|
||||
if isinstance(nid, UaNodeId):
|
||||
self._parent_node = self._client.get_node(nid)
|
||||
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)
|
||||
else:
|
||||
raise ValueError(f"无法解析父节点ID: {self._parent_node_id}")
|
||||
# 提取 ns 和 i 或 s
|
||||
m_num = re.search(r'ns=(\d+);i=(\d+)', nid)
|
||||
m_str = re.search(r'ns=(\d+);s=(.+?)(?:\)|$)', nid)
|
||||
if m_num:
|
||||
ns = int(m_num.group(1))
|
||||
identifier = int(m_num.group(2))
|
||||
node_id = NodeId(identifier, ns)
|
||||
self._parent_node = self._client.get_node(node_id)
|
||||
elif m_str:
|
||||
ns = int(m_str.group(1))
|
||||
identifier = m_str.group(2).strip()
|
||||
node_id_str = f"ns={ns};s={identifier}"
|
||||
self._parent_node = self._client.get_node(node_id_str)
|
||||
else:
|
||||
# 回退
|
||||
self._parent_node = self._client.get_node(self._parent_node_id)
|
||||
else:
|
||||
# 直接使用节点ID字符串
|
||||
self._parent_node = self._client.get_node(self._parent_node_id)
|
||||
except Exception as e:
|
||||
print(f"获取父节点失败: {self._parent_node_id}, 错误: {e}")
|
||||
|
||||
712
unilabos/devices/cameraSII/cameraDriver.py
Normal file
712
unilabos/devices/cameraSII/cameraDriver.py
Normal file
@@ -0,0 +1,712 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import websockets
|
||||
|
||||
logging.getLogger("zeep").setLevel(logging.WARNING)
|
||||
logging.getLogger("zeep.xsd.schema").setLevel(logging.WARNING)
|
||||
logging.getLogger("zeep.xsd.schema.schema").setLevel(logging.WARNING)
|
||||
from onvif import ONVIFCamera # 新增:ONVIF PTZ 控制
|
||||
|
||||
|
||||
# ======================= 独立的 PTZController =======================
|
||||
class PTZController:
|
||||
def __init__(self, host: str, port: int, user: str, password: str):
|
||||
"""
|
||||
:param host: 摄像机 IP 或域名(和 RTSP 的一样即可)
|
||||
:param port: ONVIF 端口(多数为 80,看你的设备)
|
||||
:param user: 摄像机用户名
|
||||
:param password: 摄像机密码
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.user = user
|
||||
self.password = password
|
||||
|
||||
self.cam: Optional[ONVIFCamera] = None
|
||||
self.media_service = None
|
||||
self.ptz_service = None
|
||||
self.profile = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
建立 ONVIF 连接并初始化 PTZ 能力,失败返回 False(不抛异常)
|
||||
Note: 首先 pip install onvif-zeep
|
||||
"""
|
||||
try:
|
||||
self.cam = ONVIFCamera(self.host, self.port, self.user, self.password)
|
||||
self.media_service = self.cam.create_media_service()
|
||||
self.ptz_service = self.cam.create_ptz_service()
|
||||
profiles = self.media_service.GetProfiles()
|
||||
if not profiles:
|
||||
print("[PTZ] No media profiles found on camera.", file=sys.stderr)
|
||||
return False
|
||||
self.profile = profiles[0]
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[PTZ] Failed to init ONVIF PTZ: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _continuous_move(self, pan: float, tilt: float, zoom: float, duration: float) -> bool:
|
||||
"""
|
||||
连续移动一段时间(秒),之后自动停止。
|
||||
此函数为阻塞模式:只有在 Stop 调用结束后,才返回 True/False。
|
||||
"""
|
||||
if not self.ptz_service or not self.profile:
|
||||
print("[PTZ] _continuous_move: ptz_service or profile not ready", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# 进入前先强行停一下,避免前一次残留动作
|
||||
self._force_stop()
|
||||
|
||||
req = self.ptz_service.create_type("ContinuousMove")
|
||||
req.ProfileToken = self.profile.token
|
||||
|
||||
req.Velocity = {
|
||||
"PanTilt": {"x": pan, "y": tilt},
|
||||
"Zoom": {"x": zoom},
|
||||
}
|
||||
|
||||
try:
|
||||
print(f"[PTZ] ContinuousMove start: pan={pan}, tilt={tilt}, zoom={zoom}, duration={duration}", file=sys.stderr)
|
||||
self.ptz_service.ContinuousMove(req)
|
||||
except Exception as e:
|
||||
print(f"[PTZ] ContinuousMove failed: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# 阻塞等待:这里决定“运动时间”
|
||||
import time
|
||||
wait_seconds = max(2 * duration, 0.0)
|
||||
time.sleep(wait_seconds)
|
||||
|
||||
# 运动完成后强制停止
|
||||
return self._force_stop()
|
||||
|
||||
def stop(self) -> bool:
|
||||
"""
|
||||
阻塞调用 Stop(带重试),成功 True,失败 False。
|
||||
"""
|
||||
return self._force_stop()
|
||||
|
||||
# ------- 对外动作接口(给 CameraController 调用) -------
|
||||
# 所有接口都为“阻塞模式”:只有在运动 + Stop 完成后才返回 True/False
|
||||
|
||||
def move_up(self, speed: float = 0.5, duration: float = 1.0) -> bool:
|
||||
print(f"[PTZ] move_up called, speed={speed}, duration={duration}", file=sys.stderr)
|
||||
return self._continuous_move(pan=0.0, tilt=+speed, zoom=0.0, duration=duration)
|
||||
|
||||
def move_down(self, speed: float = 0.5, duration: float = 1.0) -> bool:
|
||||
print(f"[PTZ] move_down called, speed={speed}, duration={duration}", file=sys.stderr)
|
||||
return self._continuous_move(pan=0.0, tilt=-speed, zoom=0.0, duration=duration)
|
||||
|
||||
def move_left(self, speed: float = 0.2, duration: float = 1.0) -> bool:
|
||||
print(f"[PTZ] move_left called, speed={speed}, duration={duration}", file=sys.stderr)
|
||||
return self._continuous_move(pan=-speed, tilt=0.0, zoom=0.0, duration=duration)
|
||||
|
||||
def move_right(self, speed: float = 0.2, duration: float = 1.0) -> bool:
|
||||
print(f"[PTZ] move_right called, speed={speed}, duration={duration}", file=sys.stderr)
|
||||
return self._continuous_move(pan=+speed, tilt=0.0, zoom=0.0, duration=duration)
|
||||
|
||||
# ------- 占位的变倍接口(当前设备不支持) -------
|
||||
def zoom_in(self, speed: float = 0.2, duration: float = 1.0) -> bool:
|
||||
"""
|
||||
当前设备不支持变倍;保留方法只是避免上层调用时报错。
|
||||
"""
|
||||
print("[PTZ] zoom_in is disabled for this device.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def zoom_out(self, speed: float = 0.2, duration: float = 1.0) -> bool:
|
||||
"""
|
||||
当前设备不支持变倍;保留方法只是避免上层调用时报错。
|
||||
"""
|
||||
print("[PTZ] zoom_out is disabled for this device.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _force_stop(self, retries: int = 3, delay: float = 0.1) -> bool:
|
||||
"""
|
||||
尝试多次调用 Stop,作为“强制停止”手段。
|
||||
:param retries: 重试次数
|
||||
:param delay: 每次重试间隔(秒)
|
||||
"""
|
||||
if not self.ptz_service or not self.profile:
|
||||
print("[PTZ] _force_stop: ptz_service or profile not ready", file=sys.stderr)
|
||||
return False
|
||||
|
||||
import time
|
||||
last_error = None
|
||||
for i in range(retries):
|
||||
try:
|
||||
print(f"[PTZ] _force_stop: calling Stop(), attempt={i+1}", file=sys.stderr)
|
||||
self.ptz_service.Stop({"ProfileToken": self.profile.token})
|
||||
print("[PTZ] _force_stop: Stop() returned OK", file=sys.stderr)
|
||||
return True
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
print(f"[PTZ] _force_stop: Stop() failed at attempt {i+1}: {e}", file=sys.stderr)
|
||||
time.sleep(delay)
|
||||
|
||||
print(f"[PTZ] _force_stop: all {retries} attempts failed, last error: {last_error}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# ======================= CameraController(加入 PTZ) =======================
|
||||
|
||||
class CameraController:
|
||||
"""
|
||||
Uni-Lab-OS 摄像头驱动(driver 形式)
|
||||
启动 Uni-Lab-OS 后,立即开始推流
|
||||
|
||||
- WebSocket 信令:通过 signal_backend_url 连接到后端
|
||||
例如: wss://sciol.ac.cn/api/realtime/signal/host/<host_id>
|
||||
- 媒体服务器:通过 rtmp_url / webrtc_api / webrtc_stream_url
|
||||
当前配置为 SRS,与独立 HostSimulator 独立运行脚本保持一致。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host_id: str = "demo-host",
|
||||
|
||||
# (1)信令后端(WebSocket)
|
||||
signal_backend_url: str = "wss://sciol.ac.cn/api/realtime/signal/host",
|
||||
|
||||
# (2)媒体后端(RTMP + WebRTC API)
|
||||
rtmp_url: str = "rtmp://srs.sciol.ac.cn:4499/live/camera-01",
|
||||
webrtc_api: str = "https://srs.sciol.ac.cn/rtc/v1/play/",
|
||||
webrtc_stream_url: str = "webrtc://srs.sciol.ac.cn:4500/live/camera-01",
|
||||
camera_rtsp_url: str = "",
|
||||
|
||||
# (3)PTZ 控制相关(ONVIF)
|
||||
ptz_host: str = "", # 一般就是摄像头 IP,比如 "192.168.31.164"
|
||||
ptz_port: int = 80, # ONVIF 端口,不一定是 80,按实际情况改
|
||||
ptz_user: str = "", # admin
|
||||
ptz_password: str = "", # admin123
|
||||
):
|
||||
self.host_id = host_id
|
||||
self.camera_rtsp_url = camera_rtsp_url
|
||||
|
||||
# 拼接最终的 WebSocket URL:.../host/<host_id>
|
||||
signal_backend_url = signal_backend_url.rstrip("/")
|
||||
if not signal_backend_url.endswith("/host"):
|
||||
signal_backend_url = signal_backend_url + "/host"
|
||||
self.signal_backend_url = f"{signal_backend_url}/{host_id}"
|
||||
|
||||
# 媒体服务器配置
|
||||
self.rtmp_url = rtmp_url
|
||||
self.webrtc_api = webrtc_api
|
||||
self.webrtc_stream_url = webrtc_stream_url
|
||||
|
||||
# PTZ 控制
|
||||
self.ptz_host = ptz_host
|
||||
self.ptz_port = ptz_port
|
||||
self.ptz_user = ptz_user
|
||||
self.ptz_password = ptz_password
|
||||
self._ptz: Optional[PTZController] = None
|
||||
self._init_ptz_if_possible()
|
||||
|
||||
# 运行时状态
|
||||
self._ws: Optional[object] = None
|
||||
self._ffmpeg_process: Optional[subprocess.Popen] = None
|
||||
self._running = False
|
||||
self._loop_task: Optional[asyncio.Future] = None
|
||||
|
||||
# 事件循环 & 线程
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._loop_thread: Optional[threading.Thread] = None
|
||||
|
||||
try:
|
||||
self.start()
|
||||
except Exception as e:
|
||||
print(f"[CameraController] __init__ auto start failed: {e}", file=sys.stderr)
|
||||
|
||||
# ------------------------ PTZ 初始化 ------------------------
|
||||
|
||||
# ------------------------ PTZ 公开动作方法(一个动作一个函数) ------------------------
|
||||
|
||||
def ptz_move_up(self, speed: float = 0.5, duration: float = 1.0) -> bool:
|
||||
print(f"[CameraController] ptz_move_up called, speed={speed}, duration={duration}")
|
||||
return self._ptz.move_up(speed=speed, duration=duration)
|
||||
|
||||
def ptz_move_down(self, speed: float = 0.5, duration: float = 1.0) -> bool:
|
||||
print(f"[CameraController] ptz_move_down called, speed={speed}, duration={duration}")
|
||||
return self._ptz.move_down(speed=speed, duration=duration)
|
||||
|
||||
def ptz_move_left(self, speed: float = 0.2, duration: float = 1.0) -> bool:
|
||||
print(f"[CameraController] ptz_move_left called, speed={speed}, duration={duration}")
|
||||
return self._ptz.move_left(speed=speed, duration=duration)
|
||||
|
||||
def ptz_move_right(self, speed: float = 0.2, duration: float = 1.0) -> bool:
|
||||
print(f"[CameraController] ptz_move_right called, speed={speed}, duration={duration}")
|
||||
return self._ptz.move_right(speed=speed, duration=duration)
|
||||
|
||||
def zoom_in(self, speed: float = 0.2, duration: float = 1.0) -> bool:
|
||||
"""
|
||||
当前设备不支持变倍;保留方法只是避免上层调用时报错。
|
||||
"""
|
||||
print("[PTZ] zoom_in is disabled for this device.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def zoom_out(self, speed: float = 0.2, duration: float = 1.0) -> bool:
|
||||
"""
|
||||
当前设备不支持变倍;保留方法只是避免上层调用时报错。
|
||||
"""
|
||||
print("[PTZ] zoom_out is disabled for this device.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def ptz_stop(self):
|
||||
if self._ptz is None:
|
||||
print("[CameraController] PTZ not initialized.", file=sys.stderr)
|
||||
return
|
||||
self._ptz.stop()
|
||||
|
||||
def _init_ptz_if_possible(self):
|
||||
"""
|
||||
根据 ptz_host / user / password 初始化 PTZ;
|
||||
如果配置信息不全则不启用 PTZ(静默)。
|
||||
"""
|
||||
if not (self.ptz_host and self.ptz_user and self.ptz_password):
|
||||
return
|
||||
ctrl = PTZController(
|
||||
host=self.ptz_host,
|
||||
port=self.ptz_port,
|
||||
user=self.ptz_user,
|
||||
password=self.ptz_password,
|
||||
)
|
||||
if ctrl.connect():
|
||||
self._ptz = ctrl
|
||||
else:
|
||||
self._ptz = None
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 对外暴露的方法:供 Uni-Lab-OS 调用
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def start(self, config: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
启动 Camera 连接 & 消息循环,并在启动时就开启 FFmpeg 推流,
|
||||
"""
|
||||
|
||||
if self._running:
|
||||
return {"status": "already_running", "host_id": self.host_id}
|
||||
|
||||
# 应用 config 覆盖(如果有)
|
||||
if config:
|
||||
self.camera_rtsp_url = config.get("camera_rtsp_url", self.camera_rtsp_url)
|
||||
cfg_host_id = config.get("host_id")
|
||||
if cfg_host_id:
|
||||
self.host_id = cfg_host_id
|
||||
|
||||
signal_backend_url = config.get("signal_backend_url")
|
||||
if signal_backend_url:
|
||||
signal_backend_url = signal_backend_url.rstrip("/")
|
||||
if not signal_backend_url.endswith("/host"):
|
||||
signal_backend_url = signal_backend_url + "/host"
|
||||
self.signal_backend_url = f"{signal_backend_url}/{self.host_id}"
|
||||
|
||||
self.rtmp_url = config.get("rtmp_url", self.rtmp_url)
|
||||
self.webrtc_api = config.get("webrtc_api", self.webrtc_api)
|
||||
self.webrtc_stream_url = config.get(
|
||||
"webrtc_stream_url", self.webrtc_stream_url
|
||||
)
|
||||
|
||||
# PTZ 相关配置也允许通过 config 注入
|
||||
self.ptz_host = config.get("ptz_host", self.ptz_host)
|
||||
self.ptz_port = int(config.get("ptz_port", self.ptz_port))
|
||||
self.ptz_user = config.get("ptz_user", self.ptz_user)
|
||||
self.ptz_password = config.get("ptz_password", self.ptz_password)
|
||||
self._init_ptz_if_possible()
|
||||
|
||||
self._running = True
|
||||
|
||||
# === start 时启动 FFmpeg 推流 ===
|
||||
self._start_ffmpeg()
|
||||
|
||||
# 创建新的事件循环和线程(用于 WebSocket 信令)
|
||||
self._loop = asyncio.new_event_loop()
|
||||
|
||||
def loop_runner(loop: asyncio.AbstractEventLoop):
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_forever()
|
||||
except Exception as e:
|
||||
print(f"[CameraController] event loop error: {e}", file=sys.stderr)
|
||||
|
||||
self._loop_thread = threading.Thread(
|
||||
target=loop_runner, args=(self._loop,), daemon=True
|
||||
)
|
||||
self._loop_thread.start()
|
||||
|
||||
self._loop_task = asyncio.run_coroutine_threadsafe(
|
||||
self._run_main_loop(), self._loop
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "started",
|
||||
"host_id": self.host_id,
|
||||
"signal_backend_url": self.signal_backend_url,
|
||||
"rtmp_url": self.rtmp_url,
|
||||
"webrtc_api": self.webrtc_api,
|
||||
"webrtc_stream_url": self.webrtc_stream_url,
|
||||
}
|
||||
|
||||
def stop(self) -> Dict[str, Any]:
|
||||
"""
|
||||
停止推流 & 断开 WebSocket,并关闭事件循环线程。
|
||||
"""
|
||||
self._running = False
|
||||
|
||||
self._stop_ffmpeg()
|
||||
|
||||
if self._ws and self._loop is not None:
|
||||
async def close_ws():
|
||||
try:
|
||||
await self._ws.close()
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error when closing WebSocket: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(close_ws(), self._loop)
|
||||
|
||||
if self._loop_task is not None:
|
||||
if not self._loop_task.done():
|
||||
self._loop_task.cancel()
|
||||
try:
|
||||
self._loop_task.result()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] main loop task error in stop(): {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
finally:
|
||||
self._loop_task = None
|
||||
|
||||
if self._loop is not None:
|
||||
try:
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error when stopping event loop: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if self._loop_thread is not None:
|
||||
try:
|
||||
self._loop_thread.join(timeout=5)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error when joining loop thread: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
finally:
|
||||
self._loop_thread = None
|
||||
|
||||
self._ws = None
|
||||
self._loop = None
|
||||
|
||||
return {"status": "stopped", "host_id": self.host_id}
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
查询当前状态,方便在 Uni-Lab-OS 中做监控。
|
||||
"""
|
||||
ws_closed = None
|
||||
if self._ws is not None:
|
||||
ws_closed = getattr(self._ws, "closed", None)
|
||||
|
||||
if ws_closed is None:
|
||||
websocket_connected = self._ws is not None
|
||||
else:
|
||||
websocket_connected = (self._ws is not None) and (not ws_closed)
|
||||
|
||||
return {
|
||||
"host_id": self.host_id,
|
||||
"running": self._running,
|
||||
"websocket_connected": websocket_connected,
|
||||
"ffmpeg_running": bool(
|
||||
self._ffmpeg_process and self._ffmpeg_process.poll() is None
|
||||
),
|
||||
"signal_backend_url": self.signal_backend_url,
|
||||
"rtmp_url": self.rtmp_url,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 内部实现逻辑:WebSocket 循环 / FFmpeg / WebRTC Offer 处理
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
async def _run_main_loop(self):
|
||||
try:
|
||||
while self._running:
|
||||
try:
|
||||
async with websockets.connect(self.signal_backend_url) as ws:
|
||||
self._ws = ws
|
||||
await self._recv_loop()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if self._running:
|
||||
print(
|
||||
f"[CameraController] WebSocket connection error: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
await asyncio.sleep(3)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def _recv_loop(self):
|
||||
assert self._ws is not None
|
||||
ws = self._ws
|
||||
|
||||
async for message in ws:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
except json.JSONDecodeError:
|
||||
print(
|
||||
f"[CameraController] received non-JSON message: {message}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
await self._handle_message(data)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error while handling message {data}: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
async def _handle_message(self, data: Dict[str, Any]):
|
||||
"""
|
||||
处理来自信令后端的消息:
|
||||
- command: start_stream / stop_stream / ptz_xxx
|
||||
- type: offer (WebRTC)
|
||||
"""
|
||||
cmd = data.get("command")
|
||||
|
||||
# ---------- 推流控制 ----------
|
||||
if cmd == "start_stream":
|
||||
try:
|
||||
self._start_ffmpeg()
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error when starting FFmpeg on start_stream: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
if cmd == "stop_stream":
|
||||
try:
|
||||
self._stop_ffmpeg()
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error when stopping FFmpeg on stop_stream: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
# # ---------- PTZ 控制 ----------
|
||||
# # 例如信令可以发:
|
||||
# # {"command": "ptz_move", "direction": "down", "speed": 0.5, "duration": 0.5}
|
||||
# if cmd == "ptz_move":
|
||||
# if self._ptz is None:
|
||||
# # 没有初始化 PTZ,静默忽略或打印一条
|
||||
# print("[CameraController] PTZ not initialized.", file=sys.stderr)
|
||||
# return
|
||||
|
||||
# direction = data.get("direction", "")
|
||||
# speed = float(data.get("speed", 0.5))
|
||||
# duration = float(data.get("duration", 0.5))
|
||||
|
||||
# try:
|
||||
# if direction == "up":
|
||||
# self._ptz.move_up(speed=speed, duration=duration)
|
||||
# elif direction == "down":
|
||||
# self._ptz.move_down(speed=speed, duration=duration)
|
||||
# elif direction == "left":
|
||||
# self._ptz.move_left(speed=speed, duration=duration)
|
||||
# elif direction == "right":
|
||||
# self._ptz.move_right(speed=speed, duration=duration)
|
||||
# elif direction == "zoom_in":
|
||||
# self._ptz.zoom_in(speed=speed, duration=duration)
|
||||
# elif direction == "zoom_out":
|
||||
# self._ptz.zoom_out(speed=speed, duration=duration)
|
||||
# elif direction == "stop":
|
||||
# self._ptz.stop()
|
||||
# else:
|
||||
# # 未知方向,忽略
|
||||
# pass
|
||||
# except Exception as e:
|
||||
# print(
|
||||
# f"[CameraController] error when handling PTZ move: {e}",
|
||||
# file=sys.stderr,
|
||||
# )
|
||||
# return
|
||||
|
||||
# ---------- WebRTC Offer ----------
|
||||
if data.get("type") == "offer":
|
||||
offer_sdp = data.get("sdp", "")
|
||||
camera_id = data.get("cameraId", "camera-01")
|
||||
try:
|
||||
answer_sdp = await self._handle_webrtc_offer(offer_sdp)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error when handling WebRTC offer: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
if self._ws:
|
||||
answer_payload = {
|
||||
"type": "answer",
|
||||
"sdp": answer_sdp,
|
||||
"cameraId": camera_id,
|
||||
"hostId": self.host_id,
|
||||
}
|
||||
try:
|
||||
await self._ws.send(json.dumps(answer_payload))
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error when sending WebRTC answer: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# ------------------------ FFmpeg 相关 ------------------------
|
||||
|
||||
def _start_ffmpeg(self):
|
||||
if self._ffmpeg_process and self._ffmpeg_process.poll() is None:
|
||||
return
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-rtsp_transport", "tcp",
|
||||
"-i", self.camera_rtsp_url,
|
||||
|
||||
"-c:v", "libx264",
|
||||
"-preset", "ultrafast",
|
||||
"-tune", "zerolatency",
|
||||
"-profile:v", "baseline",
|
||||
"-b:v", "1M",
|
||||
"-maxrate", "1M",
|
||||
"-bufsize", "2M",
|
||||
"-g", "10",
|
||||
"-keyint_min", "10",
|
||||
"-sc_threshold", "0",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-x264-params", "bframes=0",
|
||||
|
||||
"-c:a", "aac",
|
||||
"-ar", "44100",
|
||||
"-ac", "1",
|
||||
"-b:a", "64k",
|
||||
|
||||
"-f", "flv",
|
||||
self.rtmp_url,
|
||||
]
|
||||
|
||||
try:
|
||||
self._ffmpeg_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT,
|
||||
shell=False,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[CameraController] failed to start FFmpeg: {e}", file=sys.stderr)
|
||||
self._ffmpeg_process = None
|
||||
raise
|
||||
|
||||
def _stop_ffmpeg(self):
|
||||
proc = self._ffmpeg_process
|
||||
|
||||
if proc and proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
proc.kill()
|
||||
try:
|
||||
proc.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(
|
||||
f"[CameraController] FFmpeg process did not exit even after kill (pid={proc.pid})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] failed to kill FFmpeg process: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] error when stopping FFmpeg: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
self._ffmpeg_process = None
|
||||
|
||||
# ------------------------ WebRTC Offer 相关 ------------------------
|
||||
|
||||
async def _handle_webrtc_offer(self, offer_sdp: str) -> str:
|
||||
payload = {
|
||||
"api": self.webrtc_api,
|
||||
"streamurl": self.webrtc_stream_url,
|
||||
"sdp": offer_sdp,
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
def _do_request():
|
||||
return requests.post(
|
||||
self.webrtc_api,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
resp = await loop.run_in_executor(None, _do_request)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] failed to send offer to media server: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise
|
||||
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] media server HTTP error: {e}, "
|
||||
f"status={resp.status_code}, body={resp.text[:200]}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[CameraController] failed to parse media server JSON: {e}, "
|
||||
f"raw={resp.text[:200]}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise
|
||||
|
||||
answer_sdp = data.get("sdp", "")
|
||||
if not answer_sdp:
|
||||
msg = f"empty SDP from media server: {data}"
|
||||
print(f"[CameraController] {msg}", file=sys.stderr)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return answer_sdp
|
||||
401
unilabos/devices/cameraSII/cameraUSB.py
Normal file
401
unilabos/devices/cameraSII/cameraUSB.py
Normal file
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
import requests
|
||||
import websockets
|
||||
|
||||
|
||||
class CameraController:
|
||||
"""
|
||||
Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ)
|
||||
|
||||
- WebSocket 信令:signal_backend_url 连接到后端
|
||||
例如: wss://sciol.ac.cn/api/realtime/signal/host/<host_id>
|
||||
- 媒体服务器:RTMP 推流到 rtmp_url;WebRTC offer 转发到 SRS 的 webrtc_api
|
||||
- 视频源:本地 USB 摄像头(V4L2,默认 /dev/video0)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host_id: str = "demo-host",
|
||||
signal_backend_url: str = "wss://sciol.ac.cn/api/realtime/signal/host",
|
||||
rtmp_url: str = "rtmp://srs.sciol.ac.cn:4499/live/camera-01",
|
||||
webrtc_api: str = "https://srs.sciol.ac.cn/rtc/v1/play/",
|
||||
webrtc_stream_url: str = "webrtc://srs.sciol.ac.cn:4500/live/camera-01",
|
||||
video_device: str = "/dev/video0",
|
||||
width: int = 1280,
|
||||
height: int = 720,
|
||||
fps: int = 30,
|
||||
video_bitrate: str = "1500k",
|
||||
audio_device: Optional[str] = None, # 比如 "hw:1,0",没有音频就保持 None
|
||||
audio_bitrate: str = "64k",
|
||||
):
|
||||
self.host_id = host_id
|
||||
|
||||
# 拼接最终 WebSocket URL:.../host/<host_id>
|
||||
signal_backend_url = signal_backend_url.rstrip("/")
|
||||
if not signal_backend_url.endswith("/host"):
|
||||
signal_backend_url = signal_backend_url + "/host"
|
||||
self.signal_backend_url = f"{signal_backend_url}/{host_id}"
|
||||
|
||||
# 媒体服务器配置
|
||||
self.rtmp_url = rtmp_url
|
||||
self.webrtc_api = webrtc_api
|
||||
self.webrtc_stream_url = webrtc_stream_url
|
||||
|
||||
# 本地采集配置
|
||||
self.video_device = video_device
|
||||
self.width = int(width)
|
||||
self.height = int(height)
|
||||
self.fps = int(fps)
|
||||
self.video_bitrate = video_bitrate
|
||||
self.audio_device = audio_device
|
||||
self.audio_bitrate = audio_bitrate
|
||||
|
||||
# 运行时状态
|
||||
self._ws: Optional[object] = None
|
||||
self._ffmpeg_process: Optional[subprocess.Popen] = None
|
||||
self._running = False
|
||||
self._loop_task: Optional[asyncio.Future] = None
|
||||
|
||||
# 事件循环 & 线程
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._loop_thread: Optional[threading.Thread] = None
|
||||
|
||||
try:
|
||||
self.start()
|
||||
except Exception as e:
|
||||
print(f"[CameraController] __init__ auto start failed: {e}", file=sys.stderr)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 对外方法
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def start(self, config: Optional[Dict[str, Any]] = None):
|
||||
if self._running:
|
||||
return {"status": "already_running", "host_id": self.host_id}
|
||||
|
||||
# 应用 config 覆盖(如果有)
|
||||
if config:
|
||||
cfg_host_id = config.get("host_id")
|
||||
if cfg_host_id:
|
||||
self.host_id = cfg_host_id
|
||||
|
||||
signal_backend_url = config.get("signal_backend_url")
|
||||
if signal_backend_url:
|
||||
signal_backend_url = signal_backend_url.rstrip("/")
|
||||
if not signal_backend_url.endswith("/host"):
|
||||
signal_backend_url = signal_backend_url + "/host"
|
||||
self.signal_backend_url = f"{signal_backend_url}/{self.host_id}"
|
||||
|
||||
self.rtmp_url = config.get("rtmp_url", self.rtmp_url)
|
||||
self.webrtc_api = config.get("webrtc_api", self.webrtc_api)
|
||||
self.webrtc_stream_url = config.get("webrtc_stream_url", self.webrtc_stream_url)
|
||||
|
||||
self.video_device = config.get("video_device", self.video_device)
|
||||
self.width = int(config.get("width", self.width))
|
||||
self.height = int(config.get("height", self.height))
|
||||
self.fps = int(config.get("fps", self.fps))
|
||||
self.video_bitrate = config.get("video_bitrate", self.video_bitrate)
|
||||
self.audio_device = config.get("audio_device", self.audio_device)
|
||||
self.audio_bitrate = config.get("audio_bitrate", self.audio_bitrate)
|
||||
|
||||
self._running = True
|
||||
|
||||
print("[CameraController] start(): starting FFmpeg streaming...", file=sys.stderr)
|
||||
self._start_ffmpeg()
|
||||
|
||||
self._loop = asyncio.new_event_loop()
|
||||
|
||||
def loop_runner(loop: asyncio.AbstractEventLoop):
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_forever()
|
||||
except Exception as e:
|
||||
print(f"[CameraController] event loop error: {e}", file=sys.stderr)
|
||||
|
||||
self._loop_thread = threading.Thread(target=loop_runner, args=(self._loop,), daemon=True)
|
||||
self._loop_thread.start()
|
||||
|
||||
self._loop_task = asyncio.run_coroutine_threadsafe(self._run_main_loop(), self._loop)
|
||||
|
||||
return {
|
||||
"status": "started",
|
||||
"host_id": self.host_id,
|
||||
"signal_backend_url": self.signal_backend_url,
|
||||
"rtmp_url": self.rtmp_url,
|
||||
"webrtc_api": self.webrtc_api,
|
||||
"webrtc_stream_url": self.webrtc_stream_url,
|
||||
"video_device": self.video_device,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"fps": self.fps,
|
||||
"video_bitrate": self.video_bitrate,
|
||||
"audio_device": self.audio_device,
|
||||
}
|
||||
|
||||
def stop(self) -> Dict[str, Any]:
|
||||
self._running = False
|
||||
|
||||
# 先取消主任务(让 ws connect/sleep 尽快退出)
|
||||
if self._loop_task is not None and not self._loop_task.done():
|
||||
self._loop_task.cancel()
|
||||
|
||||
# 停止推流
|
||||
self._stop_ffmpeg()
|
||||
|
||||
# 关闭 WebSocket(在 loop 中执行)
|
||||
if self._ws and self._loop is not None:
|
||||
|
||||
async def close_ws():
|
||||
try:
|
||||
await self._ws.close()
|
||||
except Exception as e:
|
||||
print(f"[CameraController] error closing WebSocket: {e}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(close_ws(), self._loop)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 停止事件循环
|
||||
if self._loop is not None:
|
||||
try:
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
except Exception as e:
|
||||
print(f"[CameraController] error stopping loop: {e}", file=sys.stderr)
|
||||
|
||||
# 等待线程退出
|
||||
if self._loop_thread is not None:
|
||||
try:
|
||||
self._loop_thread.join(timeout=5)
|
||||
except Exception as e:
|
||||
print(f"[CameraController] error joining loop thread: {e}", file=sys.stderr)
|
||||
|
||||
self._ws = None
|
||||
self._loop_task = None
|
||||
self._loop = None
|
||||
self._loop_thread = None
|
||||
|
||||
return {"status": "stopped", "host_id": self.host_id}
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
ws_closed = None
|
||||
if self._ws is not None:
|
||||
ws_closed = getattr(self._ws, "closed", None)
|
||||
|
||||
if ws_closed is None:
|
||||
websocket_connected = self._ws is not None
|
||||
else:
|
||||
websocket_connected = (self._ws is not None) and (not ws_closed)
|
||||
|
||||
return {
|
||||
"host_id": self.host_id,
|
||||
"running": self._running,
|
||||
"websocket_connected": websocket_connected,
|
||||
"ffmpeg_running": bool(self._ffmpeg_process and self._ffmpeg_process.poll() is None),
|
||||
"signal_backend_url": self.signal_backend_url,
|
||||
"rtmp_url": self.rtmp_url,
|
||||
"video_device": self.video_device,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"fps": self.fps,
|
||||
"video_bitrate": self.video_bitrate,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# WebSocket / 信令
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
async def _run_main_loop(self):
|
||||
print("[CameraController] main loop started", file=sys.stderr)
|
||||
try:
|
||||
while self._running:
|
||||
try:
|
||||
async with websockets.connect(self.signal_backend_url) as ws:
|
||||
self._ws = ws
|
||||
print(f"[CameraController] WebSocket connected: {self.signal_backend_url}", file=sys.stderr)
|
||||
await self._recv_loop()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if self._running:
|
||||
print(f"[CameraController] WebSocket connection error: {e}", file=sys.stderr)
|
||||
await asyncio.sleep(3)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
print("[CameraController] main loop exited", file=sys.stderr)
|
||||
|
||||
async def _recv_loop(self):
|
||||
assert self._ws is not None
|
||||
ws = self._ws
|
||||
|
||||
async for message in ws:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
except json.JSONDecodeError:
|
||||
print(f"[CameraController] non-JSON message: {message}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
try:
|
||||
await self._handle_message(data)
|
||||
except Exception as e:
|
||||
print(f"[CameraController] error handling message {data}: {e}", file=sys.stderr)
|
||||
|
||||
async def _handle_message(self, data: Dict[str, Any]):
|
||||
cmd = data.get("command")
|
||||
|
||||
if cmd == "start_stream":
|
||||
self._start_ffmpeg()
|
||||
return
|
||||
|
||||
if cmd == "stop_stream":
|
||||
self._stop_ffmpeg()
|
||||
return
|
||||
|
||||
if data.get("type") == "offer":
|
||||
offer_sdp = data.get("sdp", "")
|
||||
camera_id = data.get("cameraId", "camera-01")
|
||||
|
||||
answer_sdp = await self._handle_webrtc_offer(offer_sdp)
|
||||
|
||||
if self._ws:
|
||||
answer_payload = {
|
||||
"type": "answer",
|
||||
"sdp": answer_sdp,
|
||||
"cameraId": camera_id,
|
||||
"hostId": self.host_id,
|
||||
}
|
||||
await self._ws.send(json.dumps(answer_payload))
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# FFmpeg 推流(V4L2 USB 摄像头)
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def _start_ffmpeg(self):
|
||||
if self._ffmpeg_process and self._ffmpeg_process.poll() is None:
|
||||
return
|
||||
|
||||
# 兼容性优先:不强制输入像素格式;失败再通过外部调整 width/height/fps
|
||||
video_size = f"{self.width}x{self.height}"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"warning",
|
||||
|
||||
# video input
|
||||
"-f", "v4l2",
|
||||
"-framerate", str(self.fps),
|
||||
"-video_size", video_size,
|
||||
"-i", self.video_device,
|
||||
]
|
||||
|
||||
# optional audio input
|
||||
if self.audio_device:
|
||||
cmd += [
|
||||
"-f", "alsa",
|
||||
"-i", self.audio_device,
|
||||
"-c:a", "aac",
|
||||
"-b:a", self.audio_bitrate,
|
||||
"-ar", "44100",
|
||||
"-ac", "1",
|
||||
]
|
||||
else:
|
||||
cmd += ["-an"]
|
||||
|
||||
# video encode + rtmp out
|
||||
cmd += [
|
||||
"-c:v", "libx264",
|
||||
"-preset", "ultrafast",
|
||||
"-tune", "zerolatency",
|
||||
"-profile:v", "baseline",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-b:v", self.video_bitrate,
|
||||
"-maxrate", self.video_bitrate,
|
||||
"-bufsize", "2M",
|
||||
"-g", str(max(self.fps, 10)),
|
||||
"-keyint_min", str(max(self.fps, 10)),
|
||||
"-sc_threshold", "0",
|
||||
"-x264-params", "bframes=0",
|
||||
|
||||
"-f", "flv",
|
||||
self.rtmp_url,
|
||||
]
|
||||
|
||||
print(f"[CameraController] starting FFmpeg: {' '.join(cmd)}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
# 不再丢弃日志,至少能看到 ffmpeg 报错(调试很关键)
|
||||
self._ffmpeg_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=sys.stderr,
|
||||
shell=False,
|
||||
)
|
||||
except Exception as e:
|
||||
self._ffmpeg_process = None
|
||||
print(f"[CameraController] failed to start FFmpeg: {e}", file=sys.stderr)
|
||||
|
||||
def _stop_ffmpeg(self):
|
||||
proc = self._ffmpeg_process
|
||||
if proc and proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
except Exception as e:
|
||||
print(f"[CameraController] error stopping FFmpeg: {e}", file=sys.stderr)
|
||||
self._ffmpeg_process = None
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# WebRTC offer -> SRS
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
async def _handle_webrtc_offer(self, offer_sdp: str) -> str:
|
||||
payload = {
|
||||
"api": self.webrtc_api,
|
||||
"streamurl": self.webrtc_stream_url,
|
||||
"sdp": offer_sdp,
|
||||
}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
def _do_post():
|
||||
return requests.post(self.webrtc_api, json=payload, headers=headers, timeout=10)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
resp = await loop.run_in_executor(None, _do_post)
|
||||
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
answer_sdp = data.get("sdp", "")
|
||||
if not answer_sdp:
|
||||
raise RuntimeError(f"empty SDP from media server: {data}")
|
||||
return answer_sdp
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 直接运行用于手动测试
|
||||
c = CameraController(
|
||||
host_id="demo-host",
|
||||
video_device="/dev/video0",
|
||||
width=1280,
|
||||
height=720,
|
||||
fps=30,
|
||||
video_bitrate="1500k",
|
||||
audio_device=None,
|
||||
)
|
||||
try:
|
||||
while True:
|
||||
asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
c.stop()
|
||||
51
unilabos/devices/cameraSII/cameraUSB_test.py
Normal file
51
unilabos/devices/cameraSII/cameraUSB_test.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
import json
|
||||
|
||||
from cameraUSB import CameraController
|
||||
|
||||
|
||||
def main():
|
||||
# 按你的实际情况改
|
||||
cfg = dict(
|
||||
host_id="demo-host",
|
||||
signal_backend_url="wss://sciol.ac.cn/api/realtime/signal/host",
|
||||
rtmp_url="rtmp://srs.sciol.ac.cn:4499/live/camera-01",
|
||||
webrtc_api="https://srs.sciol.ac.cn/rtc/v1/play/",
|
||||
webrtc_stream_url="webrtc://srs.sciol.ac.cn:4500/live/camera-01",
|
||||
video_device="/dev/video7",
|
||||
width=1280,
|
||||
height=720,
|
||||
fps=30,
|
||||
video_bitrate="1500k",
|
||||
audio_device=None,
|
||||
)
|
||||
|
||||
c = CameraController(**cfg)
|
||||
|
||||
# 可选:如果你不想依赖 __init__ 自动 start,可以这样显式调用:
|
||||
# c = CameraController(host_id=cfg["host_id"])
|
||||
# c.start(cfg)
|
||||
|
||||
run_seconds = 30 # 测试运行时长
|
||||
t0 = time.time()
|
||||
|
||||
try:
|
||||
while True:
|
||||
st = c.get_status()
|
||||
print(json.dumps(st, ensure_ascii=False, indent=2))
|
||||
|
||||
if time.time() - t0 >= run_seconds:
|
||||
break
|
||||
|
||||
time.sleep(2)
|
||||
except KeyboardInterrupt:
|
||||
print("Interrupted, stopping...")
|
||||
finally:
|
||||
print("Stopping controller...")
|
||||
c.stop()
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
36
unilabos/devices/cameraSII/demo_camera_pic.py
Normal file
36
unilabos/devices/cameraSII/demo_camera_pic.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import cv2
|
||||
|
||||
# 推荐把 @ 进行 URL 编码:@ -> %40
|
||||
RTSP_URL = "rtsp://admin:admin123@192.168.31.164:554/stream1"
|
||||
OUTPUT_IMAGE = "rtsp_test_frame.jpg"
|
||||
|
||||
def main():
|
||||
print(f"尝试连接 RTSP 流: {RTSP_URL}")
|
||||
cap = cv2.VideoCapture(RTSP_URL)
|
||||
|
||||
if not cap.isOpened():
|
||||
print("错误:无法打开 RTSP 流,请检查:")
|
||||
print(" 1. IP/端口是否正确")
|
||||
print(" 2. 账号密码(尤其是 @ 是否已转成 %40)是否正确")
|
||||
print(" 3. 摄像头是否允许当前主机访问(同一网段、防火墙等)")
|
||||
return
|
||||
|
||||
print("连接成功,开始读取一帧...")
|
||||
ret, frame = cap.read()
|
||||
|
||||
if not ret or frame is None:
|
||||
print("错误:已连接但未能读取到帧数据(可能是码流未开启或网络抖动)")
|
||||
cap.release()
|
||||
return
|
||||
|
||||
# 保存当前帧
|
||||
success = cv2.imwrite(OUTPUT_IMAGE, frame)
|
||||
cap.release()
|
||||
|
||||
if success:
|
||||
print(f"成功截取一帧并保存为: {OUTPUT_IMAGE}")
|
||||
else:
|
||||
print("错误:写入图片失败,请检查磁盘权限/路径")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
21
unilabos/devices/cameraSII/demo_camera_push.py
Normal file
21
unilabos/devices/cameraSII/demo_camera_push.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# run_camera_push.py
|
||||
import time
|
||||
from cameraDriver import CameraController # 这里根据你的文件名调整
|
||||
|
||||
if __name__ == "__main__":
|
||||
controller = CameraController(
|
||||
host_id="demo-host",
|
||||
signal_backend_url="wss://sciol.ac.cn/api/realtime/signal/host",
|
||||
rtmp_url="rtmp://srs.sciol.ac.cn:4499/live/camera-01",
|
||||
webrtc_api="https://srs.sciol.ac.cn/rtc/v1/play/",
|
||||
webrtc_stream_url="webrtc://srs.sciol.ac.cn:4500/live/camera-01",
|
||||
camera_rtsp_url="rtsp://admin:admin123@192.168.31.164:554/stream1",
|
||||
)
|
||||
|
||||
try:
|
||||
while True:
|
||||
status = controller.get_status()
|
||||
print(status)
|
||||
time.sleep(5)
|
||||
except KeyboardInterrupt:
|
||||
controller.stop()
|
||||
78
unilabos/devices/cameraSII/ptz_cameracontroller_test.py
Normal file
78
unilabos/devices/cameraSII/ptz_cameracontroller_test.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
使用 CameraController 来测试 PTZ:
|
||||
让摄像头按顺序向下、向上、向左、向右运动几次。
|
||||
"""
|
||||
|
||||
import time
|
||||
import sys
|
||||
|
||||
# 根据你的工程结构修改导入路径:
|
||||
# 假设 CameraController 定义在 cameraController.py 里
|
||||
from cameraDriver import CameraController
|
||||
|
||||
|
||||
def main():
|
||||
# === 根据你的实际情况填 IP、端口、账号密码 ===
|
||||
ptz_host = "192.168.31.164"
|
||||
ptz_port = 2020 # 注意要和你单独测试 PTZController 时保持一致
|
||||
ptz_user = "admin"
|
||||
ptz_password = "admin123"
|
||||
|
||||
# 1. 创建 CameraController 实例
|
||||
cam = CameraController(
|
||||
# 其他摄像机相关参数按你类的 __init__ 来补充
|
||||
ptz_host=ptz_host,
|
||||
ptz_port=ptz_port,
|
||||
ptz_user=ptz_user,
|
||||
ptz_password=ptz_password,
|
||||
)
|
||||
|
||||
# 2. 启动 / 初始化(如果你的 CameraController 有 start(config) 之类的接口)
|
||||
# 这里给一个最小的 config,重点是 PTZ 相关字段
|
||||
config = {
|
||||
"ptz_host": ptz_host,
|
||||
"ptz_port": ptz_port,
|
||||
"ptz_user": ptz_user,
|
||||
"ptz_password": ptz_password,
|
||||
}
|
||||
|
||||
try:
|
||||
cam.start(config)
|
||||
except Exception as e:
|
||||
print(f"[TEST] CameraController start() 失败: {e}", file=sys.stderr)
|
||||
return
|
||||
|
||||
# 这里可以判断一下内部 _ptz 是否初始化成功(如果你对 CameraController 做了封装)
|
||||
if getattr(cam, "_ptz", None) is None:
|
||||
print("[TEST] CameraController 内部 PTZ 未初始化成功,请检查 ptz_host/port/user/password 配置。", file=sys.stderr)
|
||||
return
|
||||
|
||||
# 3. 依次调用 CameraController 的 PTZ 方法
|
||||
# 这里假设你在 CameraController 中提供了这几个对外方法:
|
||||
# ptz_move_down / ptz_move_up / ptz_move_left / ptz_move_right
|
||||
# 如果你命名不一样,把下面调用名改成你的即可。
|
||||
|
||||
print("向下移动(通过 CameraController)...")
|
||||
cam.ptz_move_down(speed=0.5, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("向上移动(通过 CameraController)...")
|
||||
cam.ptz_move_up(speed=0.5, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("向左移动(通过 CameraController)...")
|
||||
cam.ptz_move_left(speed=0.5, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("向右移动(通过 CameraController)...")
|
||||
cam.ptz_move_right(speed=0.5, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("测试结束。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
50
unilabos/devices/cameraSII/ptz_test.py
Normal file
50
unilabos/devices/cameraSII/ptz_test.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
测试 cameraDriver.py中的 PTZController 类,让摄像头按顺序运动几次
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from cameraDriver import PTZController
|
||||
|
||||
|
||||
def main():
|
||||
# 根据你的实际情况填 IP、端口、账号密码
|
||||
host = "192.168.31.164"
|
||||
port = 80
|
||||
user = "admin"
|
||||
password = "admin123"
|
||||
|
||||
ptz = PTZController(host=host, port=port, user=user, password=password)
|
||||
|
||||
# 1. 连接摄像头
|
||||
if not ptz.connect():
|
||||
print("连接 PTZ 失败,检查 IP/用户名/密码/端口。")
|
||||
return
|
||||
|
||||
# 2. 依次测试几个动作
|
||||
# 每个动作之间 sleep 一下方便观察
|
||||
|
||||
print("向下移动...")
|
||||
ptz.move_down(speed=0.5, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("向上移动...")
|
||||
ptz.move_up(speed=0.5, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("向左移动...")
|
||||
ptz.move_left(speed=0.5, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("向右移动...")
|
||||
ptz.move_right(speed=0.5, duration=1.0)
|
||||
time.sleep(1)
|
||||
|
||||
print("测试结束。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1061,6 +1061,18 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
||||
dis_vols = [float(dis_vols)]
|
||||
else:
|
||||
dis_vols = [float(v) for v in dis_vols]
|
||||
|
||||
# 统一混合次数为标量,防止数组/列表与 int 比较时报错
|
||||
if mix_times is not None and not isinstance(mix_times, (int, float)):
|
||||
try:
|
||||
mix_times = mix_times[0] if len(mix_times) > 0 else None
|
||||
except Exception:
|
||||
try:
|
||||
mix_times = next(iter(mix_times))
|
||||
except Exception:
|
||||
pass
|
||||
if mix_times is not None:
|
||||
mix_times = int(mix_times)
|
||||
|
||||
# 识别传输模式
|
||||
num_sources = len(sources)
|
||||
|
||||
954
unilabos/devices/liquid_handling/prcxi/base_material.json
Normal file
954
unilabos/devices/liquid_handling/prcxi/base_material.json
Normal file
@@ -0,0 +1,954 @@
|
||||
[
|
||||
{
|
||||
"uuid": "3b6f33ffbf734014bcc20e3c63e124d4",
|
||||
"Code": "ZX-58-1250",
|
||||
"Name": "Tip头适配器 1250uL",
|
||||
"SummaryName": "Tip头适配器 1250uL",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 128,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 20,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"HoleDiameter": 0,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "/images/20220624015044.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 10,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 16:03:52.6583727",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-06-24 13:50:44.8123474",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "7c822592b360451fb59690e49ac6b181",
|
||||
"Code": "ZX-58-300",
|
||||
"Name": "ZHONGXI 适配器 300uL",
|
||||
"SummaryName": "ZHONGXI 适配器 300uL",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 127,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 81,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"HoleDiameter": 0,
|
||||
"Volume": 300,
|
||||
"ImagePath": "/images/20220623102838.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 10,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 16:07:53.7453351",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-06-23 10:28:38.6190575",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c",
|
||||
"Code": "ZX-58-10",
|
||||
"Name": "吸头10ul 适配器",
|
||||
"SummaryName": "吸头10ul 适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 128,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 72.3,
|
||||
"DepthNum": 0,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"HoleDiameter": 127,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 10,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 16:37:40.7073733",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-05-30 15:17:01.8231737",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 0,
|
||||
"YSpacing": 0,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "7960f49ddfe9448abadda89bd1556936",
|
||||
"Code": "ZX-001-1250",
|
||||
"Name": "1250μL Tip头",
|
||||
"SummaryName": "1250μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 118.09,
|
||||
"WidthNum": 80.7,
|
||||
"HeightNum": 107.67,
|
||||
"DepthNum": 100,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 7.95,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "/images/20220623102536.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 96,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 20:53:27.8591195",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-06-23 10:25:36.2592442",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "45f2ed3ad925484d96463d675a0ebf66",
|
||||
"Code": "ZX-001-10",
|
||||
"Name": "10μL Tip头",
|
||||
"SummaryName": "10μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 120.98,
|
||||
"WidthNum": 82.12,
|
||||
"HeightNum": 67,
|
||||
"DepthNum": 39.1,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 5,
|
||||
"Volume": 10,
|
||||
"ImagePath": "/images/20221119041031.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": -21,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 20:56:53.462015",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-11-19 16:10:31.126801",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "068b3815e36b4a72a59bae017011b29f",
|
||||
"Code": "ZX-001-10+",
|
||||
"Name": "10μL加长 Tip头",
|
||||
"SummaryName": "10μL加长 Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 122.11,
|
||||
"WidthNum": 80.05,
|
||||
"HeightNum": 58.23,
|
||||
"DepthNum": 45.1,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 60,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 7,
|
||||
"Volume": 10,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 42,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 20:57:57.331211",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-17 17:02:51.2070383",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 1,
|
||||
"Margins_X": 7.97,
|
||||
"Margins_Y": 5
|
||||
},
|
||||
{
|
||||
"uuid": "80652665f6a54402b2408d50b40398df",
|
||||
"Code": "ZX-001-1000",
|
||||
"Name": "1000μL Tip头",
|
||||
"SummaryName": "1000μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 128.09,
|
||||
"WidthNum": 85.8,
|
||||
"HeightNum": 98,
|
||||
"DepthNum": 88,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 100,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 7.95,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 47,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 20:59:20.5534915",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-05-30 14:49:53.639727",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 1,
|
||||
"Margins_X": 14.5,
|
||||
"Margins_Y": 11.4
|
||||
},
|
||||
{
|
||||
"uuid": "076250742950465b9d6ea29a225dfb00",
|
||||
"Code": "ZX-001-300",
|
||||
"Name": "300μL Tip头",
|
||||
"SummaryName": "300μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 122.11,
|
||||
"WidthNum": 80.05,
|
||||
"HeightNum": 58.23,
|
||||
"DepthNum": 45.1,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 60,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 7,
|
||||
"Volume": 300,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 11,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 21:00:24.7266192",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-17 17:02:40.6676947",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 1,
|
||||
"Margins_X": 7.97,
|
||||
"Margins_Y": 5
|
||||
},
|
||||
{
|
||||
"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7",
|
||||
"Code": "ZX-001-200",
|
||||
"Name": "200μL Tip头",
|
||||
"SummaryName": "200μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 120.98,
|
||||
"WidthNum": 82.12,
|
||||
"HeightNum": 66.9,
|
||||
"DepthNum": 52,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 30,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 5.5,
|
||||
"Volume": 200,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 19,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 21:01:17.626704",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-05-27 11:42:24.6021522",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
|
||||
"Code": "ZX-023-0.2",
|
||||
"Name": "0.2ml PCR板",
|
||||
"SummaryName": "0.2ml PCR板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 126,
|
||||
"WidthNum": 86,
|
||||
"HeightNum": 21.2,
|
||||
"DepthNum": 15.17,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 6,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": -12,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 21:06:02.7746392",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2024-02-20 16:17:16.7921748",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "ca877b8b114a4310b429d1de4aae96ee",
|
||||
"Code": "ZX-019-2.2",
|
||||
"Name": "2.2ml 深孔板",
|
||||
"SummaryName": "2.2ml 深孔板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 127.3,
|
||||
"WidthNum": 85.35,
|
||||
"HeightNum": 44,
|
||||
"DepthNum": 42,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 8.2,
|
||||
"Volume": 2200,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 34,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 21:07:16.4538022",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:11:26.3993472",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "04211a2dc93547fe9bf6121eac533650",
|
||||
"Code": "ZX-58-10000",
|
||||
"Name": "储液槽",
|
||||
"SummaryName": "储液槽",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 125.02,
|
||||
"WidthNum": 82.97,
|
||||
"HeightNum": 31.2,
|
||||
"DepthNum": 24,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"HoleDiameter": 99.33,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": -172,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-31 18:37:56.7949909",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-17 17:22:22.8543991",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 8.5,
|
||||
"Margins_Y": 5.5
|
||||
},
|
||||
{
|
||||
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
|
||||
"Code": "ZX-58-0001",
|
||||
"Name": "全裙边 PCR适配器",
|
||||
"SummaryName": "全裙边 PCR适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 125.42,
|
||||
"WidthNum": 83.13,
|
||||
"HeightNum": 15.69,
|
||||
"DepthNum": 13.41,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 5.1,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 100,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-01-02 19:21:35.8664843",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-17 17:14:36.1210193",
|
||||
"IsStright": 1,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 3,
|
||||
"Margins_X": 9.78,
|
||||
"Margins_Y": 7.72
|
||||
},
|
||||
{
|
||||
"uuid": "6bdfdd7069df453896b0806df50f2f4d",
|
||||
"Code": "ZX-ADP-001",
|
||||
"Name": "储液槽 适配器",
|
||||
"SummaryName": "储液槽 适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 133,
|
||||
"WidthNum": 91.8,
|
||||
"HeightNum": 70,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"HoleDiameter": 1,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-02-16 17:31:26.413594",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:10:58.786996",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 0,
|
||||
"YSpacing": 0,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "9a439bed8f3344549643d6b3bc5a5eb4",
|
||||
"Code": "ZX-002-300",
|
||||
"Name": "300ul深孔板适配器",
|
||||
"SummaryName": "300ul深孔板适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 136.4,
|
||||
"WidthNum": 93.8,
|
||||
"HeightNum": 96,
|
||||
"DepthNum": 7,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 8.1,
|
||||
"Volume": 300,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-06-18 15:17:42.7917763",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:10:46.1526635",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "4dc8d6ecfd0449549683b8ef815a861b",
|
||||
"Code": "ZX-002-10",
|
||||
"Name": "10ul专用深孔板适配器",
|
||||
"SummaryName": "10ul专用深孔板适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 136.5,
|
||||
"WidthNum": 93.8,
|
||||
"HeightNum": 121.5,
|
||||
"DepthNum": 7,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 8.1,
|
||||
"Volume": 10,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-06-30 09:37:31.0451435",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:10:38.5409878",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "b01627718d3341aba649baa81c2c083c",
|
||||
"Code": "Sd155",
|
||||
"Name": "爱津",
|
||||
"SummaryName": "爱津",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 125,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 64,
|
||||
"DepthNum": 45.5,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 4,
|
||||
"Volume": 20,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-11-07 08:56:30.1794274",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-11-07 09:00:29.5496845",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "adfabfffa8f24af5abfbba67b8d0f973",
|
||||
"Code": "Fhh478",
|
||||
"Name": "适配器",
|
||||
"SummaryName": "适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 120,
|
||||
"WidthNum": 90,
|
||||
"HeightNum": 86,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"HoleDiameter": 4,
|
||||
"Volume": 1000,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-11-07 09:00:10.7579131",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-11-07 09:00:10.7579134",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "730067cf07ae43849ddf4034299030e9",
|
||||
"Code": "q1",
|
||||
"Name": "废弃槽",
|
||||
"SummaryName": "废弃槽",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 126.59,
|
||||
"WidthNum": 84.87,
|
||||
"HeightNum": 103.17,
|
||||
"DepthNum": 80,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"HoleDiameter": 1,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2023-10-14 13:15:45.8172852",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-17 17:06:18.3331101",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 1,
|
||||
"YSpacing": 1,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 2.29,
|
||||
"Margins_Y": 2.64
|
||||
},
|
||||
{
|
||||
"uuid": "57b1e4711e9e4a32b529f3132fc5931f",
|
||||
"Code": "q2",
|
||||
"Name": "96深孔板",
|
||||
"SummaryName": "96深孔板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 127.3,
|
||||
"WidthNum": 85.35,
|
||||
"HeightNum": 44,
|
||||
"DepthNum": 42,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": 1,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 8.2,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2023-10-14 13:19:55.7225524",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-07-03 17:28:59.0082394",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 15,
|
||||
"Margins_Y": 10
|
||||
},
|
||||
{
|
||||
"uuid": "853dcfb6226f476e8b23c250217dc7da",
|
||||
"Code": "q3",
|
||||
"Name": "384板",
|
||||
"SummaryName": "384板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 126.6,
|
||||
"WidthNum": 84,
|
||||
"HeightNum": 9.4,
|
||||
"DepthNum": 8,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 24,
|
||||
"HoleRow": 16,
|
||||
"HoleDiameter": 3,
|
||||
"Volume": 1250,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2023-10-14 13:22:34.779818",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-10-14 13:22:34.7798181",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 4.5,
|
||||
"YSpacing": 4.5,
|
||||
"materialEnum": null,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "01953864f6f140ccaa8ddffd4f3e46f5",
|
||||
"Code": "sdfrth654",
|
||||
"Name": "4道储液槽",
|
||||
"SummaryName": "4道储液槽",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 100,
|
||||
"WidthNum": 40,
|
||||
"HeightNum": 30,
|
||||
"DepthNum": 10,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 4,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 4,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2024-02-20 14:44:25.0021372",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-03-31 15:09:30.7392062",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 27,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "026c5d5cf3d94e56b4e16b7fb53a995b",
|
||||
"Code": "22",
|
||||
"Name": "48孔深孔板",
|
||||
"SummaryName": "48孔深孔板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "",
|
||||
"LengthNum": null,
|
||||
"WidthNum": null,
|
||||
"HeightNum": null,
|
||||
"DepthNum": null,
|
||||
"StandardHeight": null,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 6,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": null,
|
||||
"Volume": 23,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2025-03-19 09:38:09.8535874",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-03-19 09:38:09.8536386",
|
||||
"IsStright": null,
|
||||
"IsGeneral": null,
|
||||
"IsControl": null,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 18.5,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 2,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "0f1639987b154e1fac78f4fb29a1f7c1",
|
||||
"Code": "12道储液槽",
|
||||
"Name": "12道储液槽",
|
||||
"SummaryName": "12道储液槽",
|
||||
"SupplyType": 1,
|
||||
"Factory": "",
|
||||
"LengthNum": 129.5,
|
||||
"WidthNum": 83.047,
|
||||
"HeightNum": 30.6,
|
||||
"DepthNum": 26.7,
|
||||
"StandardHeight": null,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 8.04,
|
||||
"Volume": 12,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2025-05-21 13:10:53.2735971",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-17 17:20:40.4460256",
|
||||
"IsStright": null,
|
||||
"IsGeneral": null,
|
||||
"IsControl": null,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 8.7,
|
||||
"Margins_Y": 5.35
|
||||
},
|
||||
{
|
||||
"uuid": "548bbc3df0d4447586f2c19d2c0c0c55",
|
||||
"Code": "HPLC01",
|
||||
"Name": "HPLC料盘",
|
||||
"SummaryName": "HPLC料盘",
|
||||
"SupplyType": 1,
|
||||
"Factory": "",
|
||||
"LengthNum": 0,
|
||||
"WidthNum": 0,
|
||||
"HeightNum": 0,
|
||||
"DepthNum": 0,
|
||||
"StandardHeight": null,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 7,
|
||||
"HoleRow": 15,
|
||||
"HoleDiameter": 0,
|
||||
"Volume": 1,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2025-07-12 17:10:43.2660127",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-07-12 17:10:43.2660131",
|
||||
"IsStright": null,
|
||||
"IsGeneral": null,
|
||||
"IsControl": null,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 12.5,
|
||||
"YSpacing": 16.5,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7",
|
||||
"Code": "1",
|
||||
"Name": "ep适配器",
|
||||
"SummaryName": "ep适配器",
|
||||
"SupplyType": 1,
|
||||
"Factory": "",
|
||||
"LengthNum": 128.04,
|
||||
"WidthNum": 85.8,
|
||||
"HeightNum": 42.66,
|
||||
"DepthNum": 38.08,
|
||||
"StandardHeight": null,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 6,
|
||||
"HoleRow": 4,
|
||||
"HoleDiameter": 10.6,
|
||||
"Volume": 1,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2025-09-03 13:31:54.1541015",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-17 17:18:03.8051993",
|
||||
"IsStright": null,
|
||||
"IsGeneral": null,
|
||||
"IsControl": null,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 21,
|
||||
"YSpacing": 18,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 3.54,
|
||||
"Margins_Y": 10.5
|
||||
},
|
||||
{
|
||||
"uuid": "a0757a90d8e44e81a68f306a608694f2",
|
||||
"Code": "ZX-58-30",
|
||||
"Name": "30mm适配器",
|
||||
"SummaryName": "30mm适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "",
|
||||
"LengthNum": 132,
|
||||
"WidthNum": 93.5,
|
||||
"HeightNum": 30,
|
||||
"DepthNum": 7,
|
||||
"StandardHeight": null,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 8.1,
|
||||
"Volume": 30,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2025-09-15 14:02:30.8094658",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-15 14:02:30.8098183",
|
||||
"IsStright": null,
|
||||
"IsGeneral": null,
|
||||
"IsControl": null,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 0,
|
||||
"Margins_X": 0,
|
||||
"Margins_Y": 0
|
||||
},
|
||||
{
|
||||
"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f",
|
||||
"Code": "ZX-78-096",
|
||||
"Name": "细菌培养皿",
|
||||
"SummaryName": "细菌培养皿",
|
||||
"SupplyType": 1,
|
||||
"Factory": "",
|
||||
"LengthNum": 124.09,
|
||||
"WidthNum": 81.89,
|
||||
"HeightNum": 13.67,
|
||||
"DepthNum": 11.2,
|
||||
"StandardHeight": null,
|
||||
"PipetteHeight": 0,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"HoleDiameter": 6.58,
|
||||
"Volume": 78,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2025-09-17 17:10:54.1859566",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2025-09-17 17:10:54.1859568",
|
||||
"IsStright": null,
|
||||
"IsGeneral": null,
|
||||
"IsControl": null,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": 4,
|
||||
"Margins_X": 9.28,
|
||||
"Margins_Y": 6.19
|
||||
}
|
||||
]
|
||||
@@ -156,7 +156,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300TipRack",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -4323,7 +4323,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -8297,7 +8297,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -8425,7 +8425,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -12496,7 +12496,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300TipRack",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -16664,7 +16664,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -20640,7 +20640,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -20671,7 +20671,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -20799,7 +20799,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -24872,7 +24872,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -28848,7 +28848,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -28879,7 +28879,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -29007,7 +29007,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -33080,7 +33080,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -37153,7 +37153,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 50,
|
||||
"size_z": 10,
|
||||
@@ -41151,6 +41151,5 @@
|
||||
"uuid": "730067cf07ae43849ddf4034299030e9"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,607 @@
|
||||
[
|
||||
{
|
||||
"Id": "1853794d-8cc1-4268-94b8-fc83e8be3ecc",
|
||||
"StartDosage": 1.0,
|
||||
"EndDosage": 55.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2126.89990234375,
|
||||
"B": 2085.300048828125,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 10
|
||||
},
|
||||
{
|
||||
"Id": "37a31398-499c-4df3-9bfe-ff92e6bc1427",
|
||||
"StartDosage": 1.0,
|
||||
"EndDosage": 303.0,
|
||||
"Aspiration": -1.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2229.6,
|
||||
"B": 3082.7,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "e602c693-e51c-4485-8788-beb3560e0599",
|
||||
"StartDosage": 303.0,
|
||||
"EndDosage": 400.0,
|
||||
"Aspiration": -0.8,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2156.6,
|
||||
"B": 9582.1,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "d7cdf777-ae58-46ab-b1ec-a5e59496bb8a",
|
||||
"StartDosage": 400.0,
|
||||
"EndDosage": 501.0,
|
||||
"Aspiration": -1.5,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2087.9,
|
||||
"B": 37256.0,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "6149a3a7-98fb-4270-83b4-4f21b5c4e8d8",
|
||||
"StartDosage": 501.0,
|
||||
"EndDosage": 600.0,
|
||||
"Aspiration": -1.5,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2185.0,
|
||||
"B": -12375.0,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "039f5735-a598-482d-b21d-b265d5e7436a",
|
||||
"StartDosage": 600.0,
|
||||
"EndDosage": 700.0,
|
||||
"Aspiration": -6.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2222.0,
|
||||
"B": -30370.0,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "80875977-ee0f-49f4-b10d-de429e57c5b8",
|
||||
"StartDosage": 700.0,
|
||||
"EndDosage": 800.0,
|
||||
"Aspiration": -6.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 1705.0,
|
||||
"B": 324436.0,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "a38afc7c-9c86-4014-a669-a7d159fb0c70",
|
||||
"StartDosage": 800.0,
|
||||
"EndDosage": 900.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2068.0,
|
||||
"B": 61331.0,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "a5ce0671-8767-4752-a04c-fdbdc3c7dc91",
|
||||
"StartDosage": 900.0,
|
||||
"EndDosage": 1001.0,
|
||||
"Aspiration": 3.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2047.2,
|
||||
"B": 78417.0,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "14daba17-0a35-474f-9f8a-e9ea6c355eb0",
|
||||
"StartDosage": 1.0,
|
||||
"EndDosage": 303.0,
|
||||
"Aspiration": -1.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2229.6,
|
||||
"B": 3082.7,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "82c2439c-79f6-4f61-9518-1b1205e44027",
|
||||
"StartDosage": 303.0,
|
||||
"EndDosage": 400.0,
|
||||
"Aspiration": -0.8,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2156.6,
|
||||
"B": 9582.1,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "7981db10-4005-4c62-a22d-fac90875e91c",
|
||||
"StartDosage": 400.0,
|
||||
"EndDosage": 501.0,
|
||||
"Aspiration": -1.5,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2087.9,
|
||||
"B": 37256.0,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "ae7606fd-98fa-4236-bec4-a4d60018dbea",
|
||||
"StartDosage": 501.0,
|
||||
"EndDosage": 600.0,
|
||||
"Aspiration": -1.5,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2185.0,
|
||||
"B": -12375.0,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "ed2a2db0-77b6-4a0a-ac36-7184f0b2c2c8",
|
||||
"StartDosage": 600.0,
|
||||
"EndDosage": 700.0,
|
||||
"Aspiration": -6.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2222.0,
|
||||
"B": -30370.0,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "ed639da4-b02f-4d2a-825d-b47cebdfbf1b",
|
||||
"StartDosage": 700.0,
|
||||
"EndDosage": 800.0,
|
||||
"Aspiration": -6.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 1705.0,
|
||||
"B": 324436.0,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "7e740c8a-1043-4db1-820f-2e6e77386d7f",
|
||||
"StartDosage": 800.0,
|
||||
"EndDosage": 900.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2068.0,
|
||||
"B": 61331.0,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "49b6c4fe-e11a-4056-8de7-fd9a2b81bc90",
|
||||
"StartDosage": 900.0,
|
||||
"EndDosage": 1001.0,
|
||||
"Aspiration": 3.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2047.2,
|
||||
"B": 78417.0,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "67dee69d-a2a9-4598-8d8d-98b211a58821",
|
||||
"StartDosage": 1.0,
|
||||
"EndDosage": 6.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 20211.0,
|
||||
"B": 10779.0,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 50
|
||||
},
|
||||
{
|
||||
"Id": "d5c1b2b0-f897-4873-86bf-0ce5f443dfd3",
|
||||
"StartDosage": 6.0,
|
||||
"EndDosage": 25.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 20211.0,
|
||||
"B": 10779.0,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 50
|
||||
},
|
||||
{
|
||||
"Id": "b2789b53-6e0e-4b83-9932-f41c83d10da8",
|
||||
"StartDosage": 25.0,
|
||||
"EndDosage": 50.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 20015.0,
|
||||
"B": 17507.0,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 50
|
||||
},
|
||||
{
|
||||
"Id": "1f0d0bbb-6ea2-4d19-8452-6824fa1f474c",
|
||||
"StartDosage": 0.1,
|
||||
"EndDosage": 5.0,
|
||||
"Aspiration": -1.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 1981.1,
|
||||
"B": 3498.1,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "c58111db-dadc-43bd-97b3-a596f441d704",
|
||||
"StartDosage": 5.0,
|
||||
"EndDosage": 10.0,
|
||||
"Aspiration": -1.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2113.3,
|
||||
"B": 2810.8,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "a15fd33d-28cd-4bca-bd6c-018e3bafcb65",
|
||||
"StartDosage": 10.0,
|
||||
"EndDosage": 50.0,
|
||||
"Aspiration": -0.8,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2113.3,
|
||||
"B": 2810.8,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "ab957383-d83d-4fcc-8373-9d8f415c3023",
|
||||
"StartDosage": 50.0,
|
||||
"EndDosage": 100.0,
|
||||
"Aspiration": -0.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2093.7,
|
||||
"B": 2969.2,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "be6b6f79-222f-4f6f-ae73-e537f397a11e",
|
||||
"StartDosage": 100.0,
|
||||
"EndDosage": 150.0,
|
||||
"Aspiration": 1.7,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2093.7,
|
||||
"B": 2969.2,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "0ab3fc05-8f9f-4dc0-a2ce-918ade17810c",
|
||||
"StartDosage": 150.0,
|
||||
"EndDosage": 200.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "43b82710-37df-4039-9513-aa49bc5bc607",
|
||||
"StartDosage": 200.0,
|
||||
"EndDosage": 250.0,
|
||||
"Aspiration": 4.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "2f208ffc-808f-4bf9-b443-14dbf0338d83",
|
||||
"StartDosage": 250.0,
|
||||
"EndDosage": 310.0,
|
||||
"Aspiration": 5.3,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "84bb5356-481d-41b9-a563-917e64b5e20c",
|
||||
"StartDosage": 1.0,
|
||||
"EndDosage": 10.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 964.19,
|
||||
"B": 1207.7,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "67463c2c-a520-4d33-831f-e0c3cdcdec60",
|
||||
"StartDosage": 10.0,
|
||||
"EndDosage": 50.0,
|
||||
"Aspiration": 0.5,
|
||||
"Dispensing": 0.0,
|
||||
"K": 964.19,
|
||||
"B": 1207.7,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "a752d77e-7c5d-450a-8b54-e87513facda0",
|
||||
"StartDosage": 50.0,
|
||||
"EndDosage": 100.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 964.19,
|
||||
"B": 1207.7,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "d30f522a-5992-4be4-984d-0c27b9e8f410",
|
||||
"StartDosage": 100.0,
|
||||
"EndDosage": 300.0,
|
||||
"Aspiration": 1.8,
|
||||
"Dispensing": 0.0,
|
||||
"K": 937.8,
|
||||
"B": 3550.1,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "29914cbe-ad35-4712-80b1-8c4e54f9fc15",
|
||||
"StartDosage": 300.0,
|
||||
"EndDosage": 500.0,
|
||||
"Aspiration": 2.5,
|
||||
"Dispensing": 0.0,
|
||||
"K": 937.8,
|
||||
"B": 3550.1,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "b75b1d6d-9b53-4b5c-b6ab-640cb23491d8",
|
||||
"StartDosage": 500.0,
|
||||
"EndDosage": 800.0,
|
||||
"Aspiration": 50.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 928.69,
|
||||
"B": 8253.7,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "1658a9de-bb62-4dd6-9715-0e8e71b27f97",
|
||||
"StartDosage": 800.0,
|
||||
"EndDosage": 900.0,
|
||||
"Aspiration": 4.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 928.69,
|
||||
"B": 8253.7,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "4d0fec65-983d-47f6-82fe-723bb9efd42a",
|
||||
"StartDosage": 900.0,
|
||||
"EndDosage": 1050.0,
|
||||
"Aspiration": 5.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 928.69,
|
||||
"B": 8253.7,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 1000
|
||||
},
|
||||
{
|
||||
"Id": "f194ad17-3be3-4684-bf21-d458693e640c",
|
||||
"StartDosage": 1.0,
|
||||
"EndDosage": 2.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 62616.0,
|
||||
"B": 106.49,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 10
|
||||
},
|
||||
{
|
||||
"Id": "fa43155c-8220-4ead-bc8f-6984a25711bf",
|
||||
"StartDosage": 2.0,
|
||||
"EndDosage": 7.0,
|
||||
"Aspiration": -0.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 52421.0,
|
||||
"B": 20977.0,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 10
|
||||
},
|
||||
{
|
||||
"Id": "9b05eebb-ba5d-427c-bd4f-1b6745bab932",
|
||||
"StartDosage": 7.0,
|
||||
"EndDosage": 11.0,
|
||||
"Aspiration": 0.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 51942.0,
|
||||
"B": 21434.0,
|
||||
"compensateEnum": 5,
|
||||
"materialVolume": 10
|
||||
},
|
||||
{
|
||||
"Id": "d4715f09-e24a-4ed2-b784-09256640bcf7",
|
||||
"StartDosage": 0.5,
|
||||
"EndDosage": 5.0,
|
||||
"Aspiration": -1.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 1981.1,
|
||||
"B": 3498.1,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "e37e2fad-954d-4a17-8312-e08bbde00902",
|
||||
"StartDosage": 5.0,
|
||||
"EndDosage": 10.0,
|
||||
"Aspiration": -1.1,
|
||||
"Dispensing": -0.8,
|
||||
"K": 2113.3,
|
||||
"B": 2810.8,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "642714bd-22c6-46b5-9a48-2f0bcd91d555",
|
||||
"StartDosage": 10.0,
|
||||
"EndDosage": 50.0,
|
||||
"Aspiration": -0.8,
|
||||
"Dispensing": -2.0,
|
||||
"K": 2113.3,
|
||||
"B": 2810.8,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "2fccf79f-52e5-4b6c-be6e-bdac167dd40c",
|
||||
"StartDosage": 50.0,
|
||||
"EndDosage": 100.0,
|
||||
"Aspiration": -0.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2093.7,
|
||||
"B": 2969.2,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "34555f2c-2e11-4c45-b733-83a8185727da",
|
||||
"StartDosage": 100.0,
|
||||
"EndDosage": 150.0,
|
||||
"Aspiration": 1.7,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2093.7,
|
||||
"B": 2969.2,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "9353ac79-b710-49da-a423-4bfe651ac16a",
|
||||
"StartDosage": 150.0,
|
||||
"EndDosage": 200.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "1628da53-8c86-4eff-b119-07cb7a859bb6",
|
||||
"StartDosage": 200.0,
|
||||
"EndDosage": 250.0,
|
||||
"Aspiration": 4.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "658913c3-2c3e-4e14-9eb3-0489b5fdee7f",
|
||||
"StartDosage": 250.0,
|
||||
"EndDosage": 310.0,
|
||||
"Aspiration": -11.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 7,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "f736e716-ec13-432c-ac2e-4905753ac6f9",
|
||||
"StartDosage": 0.1,
|
||||
"EndDosage": 5.0,
|
||||
"Aspiration": -1.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 1981.1,
|
||||
"B": 3498.1,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "7595eda8-f2d8-491f-bdac-69d169308ab5",
|
||||
"StartDosage": 5.0,
|
||||
"EndDosage": 10.0,
|
||||
"Aspiration": -1.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2113.3,
|
||||
"B": 2810.8,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "42eddd0a-8394-4245-8ad3-49573b25286e",
|
||||
"StartDosage": 10.0,
|
||||
"EndDosage": 50.0,
|
||||
"Aspiration": -0.8,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2113.3,
|
||||
"B": 2810.8,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "713eadfe-25c0-4ec0-acfd-900df9e12396",
|
||||
"StartDosage": 50.0,
|
||||
"EndDosage": 100.0,
|
||||
"Aspiration": -0.1,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2093.7,
|
||||
"B": 2969.2,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "f602c7bd-bdcf-4be0-9d77-a16d409bc64b",
|
||||
"StartDosage": 100.0,
|
||||
"EndDosage": 150.0,
|
||||
"Aspiration": 1.7,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2093.7,
|
||||
"B": 2969.2,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "b91867e5-f0a2-4bbe-b37e-aec9837b019e",
|
||||
"StartDosage": 150.0,
|
||||
"EndDosage": 200.0,
|
||||
"Aspiration": 0.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "bd2e39d7-eb93-4d40-b0b4-2aac6b5678f3",
|
||||
"StartDosage": 200.0,
|
||||
"EndDosage": 250.0,
|
||||
"Aspiration": 4.0,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 300
|
||||
},
|
||||
{
|
||||
"Id": "52e20b7f-f519-434f-86bb-a48238c290d1",
|
||||
"StartDosage": 250.0,
|
||||
"EndDosage": 310.0,
|
||||
"Aspiration": 5.3,
|
||||
"Dispensing": 0.0,
|
||||
"K": 2085.0,
|
||||
"B": 3548.3,
|
||||
"compensateEnum": 6,
|
||||
"materialVolume": 300
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,794 @@
|
||||
[
|
||||
{
|
||||
"uuid": "3b6f33ffbf734014bcc20e3c63e124d4",
|
||||
"Code": "ZX-58-1250",
|
||||
"Name": "Tip头适配器 1250uL",
|
||||
"SummaryName": "Tip头适配器 1250uL",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 128,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 20,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"ChannelNum": 1,
|
||||
"HoleDiameter": 0,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "/images/20220624015044.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 10,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 16:03:52.6583727",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-06-24 13:50:44.8123474",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "7c822592b360451fb59690e49ac6b181",
|
||||
"Code": "ZX-58-300",
|
||||
"Name": "ZHONGXI 适配器 300uL",
|
||||
"SummaryName": "ZHONGXI 适配器 300uL",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 127,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 81,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"ChannelNum": 1,
|
||||
"HoleDiameter": 0,
|
||||
"Volume": 300,
|
||||
"ImagePath": "/images/20220623102838.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 10,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 16:07:53.7453351",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-06-23 10:28:38.6190575",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c",
|
||||
"Code": "ZX-58-10",
|
||||
"Name": "吸头10ul 适配器",
|
||||
"SummaryName": "吸头10ul 适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 128,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 81,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"ChannelNum": 1,
|
||||
"HoleDiameter": 127,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "/images/20221115010348.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 10,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 16:37:40.7073733",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-11-15 13:03:48.1679642",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "7960f49ddfe9448abadda89bd1556936",
|
||||
"Code": "ZX-001-1250",
|
||||
"Name": "1250μL Tip头",
|
||||
"SummaryName": "1250μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 118.09,
|
||||
"WidthNum": 80.7,
|
||||
"HeightNum": 107.67,
|
||||
"DepthNum": 100,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 8,
|
||||
"HoleDiameter": 7.95,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "/images/20220623102536.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 96,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 20:53:27.8591195",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-06-23 10:25:36.2592442",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "45f2ed3ad925484d96463d675a0ebf66",
|
||||
"Code": "ZX-001-10",
|
||||
"Name": "10μL Tip头",
|
||||
"SummaryName": "10μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 120.98,
|
||||
"WidthNum": 82.12,
|
||||
"HeightNum": 67,
|
||||
"DepthNum": 39.1,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 8,
|
||||
"HoleDiameter": 5,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "/images/20221119041031.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": -21,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 20:56:53.462015",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-11-19 16:10:31.126801",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "068b3815e36b4a72a59bae017011b29f",
|
||||
"Code": "ZX-001-10+",
|
||||
"Name": "10μL加长 Tip头",
|
||||
"SummaryName": "10μL加长 Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 120.98,
|
||||
"WidthNum": 82.12,
|
||||
"HeightNum": 50.3,
|
||||
"DepthNum": 45.8,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 8,
|
||||
"HoleDiameter": 5,
|
||||
"Volume": 20,
|
||||
"ImagePath": "/images/20220718120113.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 42,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 20:57:57.331211",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-07-18 12:01:13.2131453",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "80652665f6a54402b2408d50b40398df",
|
||||
"Code": "ZX-001-1000",
|
||||
"Name": "1000μL Tip头",
|
||||
"SummaryName": "1000μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 118.09,
|
||||
"WidthNum": 80.7,
|
||||
"HeightNum": 107.67,
|
||||
"DepthNum": 88,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 8,
|
||||
"HoleDiameter": 7.95,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 47,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 20:59:20.5534915",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:11:44.8670189",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "076250742950465b9d6ea29a225dfb00",
|
||||
"Code": "ZX-001-300",
|
||||
"Name": "300μL Tip头",
|
||||
"SummaryName": "300μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 120.98,
|
||||
"WidthNum": 82.12,
|
||||
"HeightNum": 40,
|
||||
"DepthNum": 59.3,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 8,
|
||||
"HoleDiameter": 5.5,
|
||||
"Volume": 300,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 11,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 21:00:24.7266192",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2024-02-01 15:48:02.1562734",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7",
|
||||
"Code": "ZX-001-200",
|
||||
"Name": "200μL Tip头",
|
||||
"SummaryName": "200μL Tip头",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 120.98,
|
||||
"WidthNum": 82.12,
|
||||
"HeightNum": 66.9,
|
||||
"DepthNum": 52,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 8,
|
||||
"HoleDiameter": 5.5,
|
||||
"Volume": 200,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 19,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 21:01:17.626704",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-10-14 13:44:41.5428946",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
|
||||
"Code": "ZX-023-0.2",
|
||||
"Name": "0.2ml PCR板",
|
||||
"SummaryName": "0.2ml PCR板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 126,
|
||||
"WidthNum": 86,
|
||||
"HeightNum": 21.2,
|
||||
"DepthNum": 15.17,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 96,
|
||||
"HoleDiameter": 6,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": -12,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 21:06:02.7746392",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2024-02-20 16:17:16.7921748",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "ca877b8b114a4310b429d1de4aae96ee",
|
||||
"Code": "ZX-019-2.2",
|
||||
"Name": "2.2ml 深孔板",
|
||||
"SummaryName": "2.2ml 深孔板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 127.3,
|
||||
"WidthNum": 85.35,
|
||||
"HeightNum": 44,
|
||||
"DepthNum": 42,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 8,
|
||||
"HoleDiameter": 8.2,
|
||||
"Volume": 2200,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": 34,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-30 21:07:16.4538022",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:11:26.3993472",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "04211a2dc93547fe9bf6121eac533650",
|
||||
"Code": "ZX-58-10000",
|
||||
"Name": "储液槽",
|
||||
"SummaryName": "储液槽",
|
||||
"SupplyType": 1,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 127,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 31.2,
|
||||
"DepthNum": 24,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"ChannelNum": 1,
|
||||
"HoleDiameter": 127,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "/images/20220623103134.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": -172,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2021-12-31 18:37:56.7949909",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-06-23 10:31:34.4261358",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
|
||||
"Code": "ZX-58-0001",
|
||||
"Name": "半裙边 PCR适配器",
|
||||
"SummaryName": "半裙边 PCR适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 127,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 88,
|
||||
"DepthNum": 5,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 96,
|
||||
"HoleDiameter": 9,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "/images/20221123051800.jpg",
|
||||
"QRCode": null,
|
||||
"Qty": 100,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-01-02 19:21:35.8664843",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-11-23 17:18:00.8826719",
|
||||
"IsStright": 1,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 1,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "6bdfdd7069df453896b0806df50f2f4d",
|
||||
"Code": "ZX-ADP-001",
|
||||
"Name": "储液槽 适配器",
|
||||
"SummaryName": "储液槽 适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 133,
|
||||
"WidthNum": 91.8,
|
||||
"HeightNum": 70,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"ChannelNum": 8,
|
||||
"HoleDiameter": 1,
|
||||
"Volume": 1250,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-02-16 17:31:26.413594",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:10:58.786996",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 0,
|
||||
"YSpacing": 0,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "9a439bed8f3344549643d6b3bc5a5eb4",
|
||||
"Code": "ZX-002-300",
|
||||
"Name": "300ul深孔板适配器",
|
||||
"SummaryName": "300ul深孔板适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 136.4,
|
||||
"WidthNum": 93.8,
|
||||
"HeightNum": 96,
|
||||
"DepthNum": 7,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 96,
|
||||
"HoleDiameter": 8.1,
|
||||
"Volume": 300,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-06-18 15:17:42.7917763",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:10:46.1526635",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "4dc8d6ecfd0449549683b8ef815a861b",
|
||||
"Code": "ZX-002-10",
|
||||
"Name": "10ul专用深孔板适配器",
|
||||
"SummaryName": "10ul专用深孔板适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "宁静致远",
|
||||
"LengthNum": 136.5,
|
||||
"WidthNum": 93.8,
|
||||
"HeightNum": 121.5,
|
||||
"DepthNum": 7,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 96,
|
||||
"HoleDiameter": 8.1,
|
||||
"Volume": 10,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-06-30 09:37:31.0451435",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-08-12 13:10:38.5409878",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "b01627718d3341aba649baa81c2c083c",
|
||||
"Code": "Sd155",
|
||||
"Name": "爱津",
|
||||
"SummaryName": "爱津",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 125,
|
||||
"WidthNum": 85,
|
||||
"HeightNum": 64,
|
||||
"DepthNum": 45.5,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 1,
|
||||
"HoleDiameter": 4,
|
||||
"Volume": 20,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-11-07 08:56:30.1794274",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-11-07 09:00:29.5496845",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "adfabfffa8f24af5abfbba67b8d0f973",
|
||||
"Code": "Fhh478",
|
||||
"Name": "适配器",
|
||||
"SummaryName": "适配器",
|
||||
"SupplyType": 2,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 120,
|
||||
"WidthNum": 90,
|
||||
"HeightNum": 86,
|
||||
"DepthNum": 4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"ChannelNum": 1,
|
||||
"HoleDiameter": 4,
|
||||
"Volume": 1000,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2022-11-07 09:00:10.7579131",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2022-11-07 09:00:10.7579134",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": null,
|
||||
"YSpacing": null,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "1592e84a07f74668af155588867f2da7",
|
||||
"Code": "12",
|
||||
"Name": "12",
|
||||
"SummaryName": "12",
|
||||
"SupplyType": 1,
|
||||
"Factory": "12",
|
||||
"LengthNum": 1,
|
||||
"WidthNum": 1,
|
||||
"HeightNum": 1,
|
||||
"DepthNum": 100,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 8,
|
||||
"HoleRow": 12,
|
||||
"ChannelNum": 12,
|
||||
"HoleDiameter": 7,
|
||||
"Volume": 12,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2023-10-08 09:35:19.281766",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-10-08 09:35:19.2817667",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 0,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "730067cf07ae43849ddf4034299030e9",
|
||||
"Code": "q1",
|
||||
"Name": "废弃槽",
|
||||
"SummaryName": "废弃槽",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 190,
|
||||
"WidthNum": 135,
|
||||
"HeightNum": 75,
|
||||
"DepthNum": 1,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 1,
|
||||
"HoleRow": 1,
|
||||
"ChannelNum": 1,
|
||||
"HoleDiameter": 1,
|
||||
"Volume": 1250,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2023-10-14 13:15:45.8172852",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-10-14 13:15:45.8172869",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 1,
|
||||
"YSpacing": 1,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "57b1e4711e9e4a32b529f3132fc5931f",
|
||||
"Code": "q2",
|
||||
"Name": "96深孔板",
|
||||
"SummaryName": "96深孔板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 126.5,
|
||||
"WidthNum": 84.5,
|
||||
"HeightNum": 41.4,
|
||||
"DepthNum": 38.4,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 12,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 96,
|
||||
"HoleDiameter": 8.3,
|
||||
"Volume": 1250,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2023-10-14 13:19:55.7225524",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-10-14 13:19:55.7225525",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 9,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "853dcfb6226f476e8b23c250217dc7da",
|
||||
"Code": "q3",
|
||||
"Name": "384板",
|
||||
"SummaryName": "384板",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 126.6,
|
||||
"WidthNum": 84,
|
||||
"HeightNum": 9.4,
|
||||
"DepthNum": 8,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 24,
|
||||
"HoleRow": 16,
|
||||
"ChannelNum": 384,
|
||||
"HoleDiameter": 3,
|
||||
"Volume": 1250,
|
||||
"ImagePath": null,
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2023-10-14 13:22:34.779818",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2023-10-14 13:22:34.7798181",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 4.5,
|
||||
"YSpacing": 4.5,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "e201e206fcfc4e8ab51946a22e8cd1bc",
|
||||
"Code": "1",
|
||||
"Name": "ep",
|
||||
"SummaryName": "ep",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 504,
|
||||
"WidthNum": 337,
|
||||
"HeightNum": 160,
|
||||
"DepthNum": 163,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 6,
|
||||
"HoleRow": 4,
|
||||
"ChannelNum": 24,
|
||||
"HoleDiameter": 41.2,
|
||||
"Volume": 1,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2024-01-20 13:14:38.0308919",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2024-02-05 16:27:07.2582693",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 21,
|
||||
"YSpacing": 18,
|
||||
"materialEnum": null
|
||||
},
|
||||
{
|
||||
"uuid": "01953864f6f140ccaa8ddffd4f3e46f5",
|
||||
"Code": "sdfrth654",
|
||||
"Name": "4道储液槽",
|
||||
"SummaryName": "4道储液槽",
|
||||
"SupplyType": 1,
|
||||
"Factory": "中析",
|
||||
"LengthNum": 100,
|
||||
"WidthNum": 40,
|
||||
"HeightNum": 30,
|
||||
"DepthNum": 10,
|
||||
"StandardHeight": 0,
|
||||
"PipetteHeight": null,
|
||||
"HoleColum": 4,
|
||||
"HoleRow": 8,
|
||||
"ChannelNum": 4,
|
||||
"HoleDiameter": 4,
|
||||
"Volume": 1000,
|
||||
"ImagePath": "",
|
||||
"QRCode": null,
|
||||
"Qty": null,
|
||||
"CreateName": null,
|
||||
"CreateTime": "2024-02-20 14:44:25.0021372",
|
||||
"UpdateName": null,
|
||||
"UpdateTime": "2024-02-20 15:28:21.3881302",
|
||||
"IsStright": 0,
|
||||
"IsGeneral": 1,
|
||||
"IsControl": 0,
|
||||
"ArmCode": null,
|
||||
"XSpacing": 27,
|
||||
"YSpacing": 9,
|
||||
"materialEnum": null
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,602 @@
|
||||
[
|
||||
{
|
||||
"uuid": "87ea11eeb24b43648ce294654b561fe7",
|
||||
"PlanName": "2341",
|
||||
"PlanCode": "2980eb",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-05-15 18:24:00.8445073",
|
||||
"MatrixId": "34ba3f02-6fcd-48e6-bb8e-3b0ce1d54ed5"
|
||||
},
|
||||
{
|
||||
"uuid": "0a977d6ebc4244739793b0b6f8b3f815",
|
||||
"PlanName": "384测试方案(300模块)",
|
||||
"PlanCode": "9336ee",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-06-13 10:34:52.5310959",
|
||||
"MatrixId": "74ed84ea-0b5d-4307-a966-ceb83fcaefe7"
|
||||
},
|
||||
{
|
||||
"uuid": "aff2cd213ad34072b370f44acb5ab658",
|
||||
"PlanName": "96孔吸300方案(单放)",
|
||||
"PlanCode": "9932fc",
|
||||
"PlanTarget": "测试用",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-06-13 09:57:38.422353",
|
||||
"MatrixId": "bacd78be-b86d-49d6-973a-dd522834e4c4"
|
||||
},
|
||||
{
|
||||
"uuid": "97816d94f99a48409379013d19f0ab66",
|
||||
"PlanName": "384测试方案(50模块)",
|
||||
"PlanCode": "3964de",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-06-13 10:32:22.8918817",
|
||||
"MatrixId": "74ed84ea-0b5d-4307-a966-ceb83fcaefe7"
|
||||
},
|
||||
{
|
||||
"uuid": "c3d86e9d7eed4ddb8c32e9234da659de",
|
||||
"PlanName": "96吸50方案(单放)",
|
||||
"PlanCode": "6994aa",
|
||||
"PlanTarget": "测试用",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-08-08 11:50:14.6850189",
|
||||
"MatrixId": "bacd78be-b86d-49d6-973a-dd522834e4c4"
|
||||
},
|
||||
{
|
||||
"uuid": "59a97f77718d4bbba6bed1ddbf959772",
|
||||
"PlanName": "test12",
|
||||
"PlanCode": "8630fa",
|
||||
"PlanTarget": "12通道",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-08 09:36:14.2536629",
|
||||
"MatrixId": "517c836e-56c6-4c06-a897-7074886061bd"
|
||||
},
|
||||
{
|
||||
"uuid": "84d50e4cf3034aa6a3de505a92b30812",
|
||||
"PlanName": "test001",
|
||||
"PlanCode": "9013fe",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-08 16:37:57.2302499",
|
||||
"MatrixId": "ed9b1ceb-b879-4b8c-a246-2d4f54fbe970"
|
||||
},
|
||||
{
|
||||
"uuid": "d052b893c6324ae38d301a58614a5663",
|
||||
"PlanName": "test01",
|
||||
"PlanCode": "8524cf",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-09 11:00:21.4973895",
|
||||
"MatrixId": "bacd78be-b86d-49d6-973a-dd522834e4c4"
|
||||
},
|
||||
{
|
||||
"uuid": "875a6eaa00e548b99318fd0be310e879",
|
||||
"PlanName": "test002",
|
||||
"PlanCode": "2477fe",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-09 11:02:01.2027308",
|
||||
"MatrixId": "7374dc89-d425-42aa-b252-1b1338d3c2f2"
|
||||
},
|
||||
{
|
||||
"uuid": "ecb3cb37f603495d95a93522a6b611e3",
|
||||
"PlanName": "test02",
|
||||
"PlanCode": "5126cb",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-09 11:02:14.7987877",
|
||||
"MatrixId": "7374dc89-d425-42aa-b252-1b1338d3c2f2"
|
||||
},
|
||||
{
|
||||
"uuid": "705edabbcbd645d0925e4e581643247c",
|
||||
"PlanName": "test003",
|
||||
"PlanCode": "4994cc",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-09 11:41:04.1715458",
|
||||
"MatrixId": "4c126841-5c37-49c7-b4e8-539983bc9cc4"
|
||||
},
|
||||
{
|
||||
"uuid": "6c58136d7de54a6abb7b51e6327eacac",
|
||||
"PlanName": "test04",
|
||||
"PlanCode": "9704dd",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-09 11:51:59.1752071",
|
||||
"MatrixId": "4c126841-5c37-49c7-b4e8-539983bc9cc4"
|
||||
},
|
||||
{
|
||||
"uuid": "208f00a911b846d9922b2e72bdda978c",
|
||||
"PlanName": "96版位 50ul量程",
|
||||
"PlanCode": "7595be",
|
||||
"PlanTarget": "213213",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-18 19:12:17.4641981",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "40bd0ca25ffb4be6b246353db6ebefc9",
|
||||
"PlanName": "96版位 300ul量程",
|
||||
"PlanCode": "7421fc",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-14 14:47:03.8105699",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "30b838bb7d124ec885b506df29ee7860",
|
||||
"PlanName": "300版位 50ul量程",
|
||||
"PlanCode": "6364cc",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-14 14:48:05.2235254",
|
||||
"MatrixId": "f8c70333-b717-4ca0-9306-c40fd5f156fb"
|
||||
},
|
||||
{
|
||||
"uuid": "e53c591c86334c6f92d3b1afa107bcf8",
|
||||
"PlanName": "384版位 300ul量程",
|
||||
"PlanCode": "4029be",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-14 14:47:48.9478679",
|
||||
"MatrixId": "f8c70333-b717-4ca0-9306-c40fd5f156fb"
|
||||
},
|
||||
{
|
||||
"uuid": "1d26d1ab45c6431990ba0e00cc1f78d2",
|
||||
"PlanName": "96版位梯度稀释 50ul量程",
|
||||
"PlanCode": "3502cf",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-14 14:48:12.8676989",
|
||||
"MatrixId": "916bbd00-e66c-4237-9843-e049b70b740a"
|
||||
},
|
||||
{
|
||||
"uuid": "7a0383b4fbb543339723513228365451",
|
||||
"PlanName": "96版位梯度稀释 300ul量程",
|
||||
"PlanCode": "9345fe",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-10-14 14:50:02.0250566",
|
||||
"MatrixId": "916bbd00-e66c-4237-9843-e049b70b740a"
|
||||
},
|
||||
{
|
||||
"uuid": "69d4882f0f024fb5a3b91010f149ff89",
|
||||
"PlanName": "测试",
|
||||
"PlanCode": "3941bf",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2023-12-11 15:24:30.1371824",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "3603f89f4e0945f68353a33e8017ba6e",
|
||||
"PlanName": "测试111",
|
||||
"PlanCode": "8056eb",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 09:29:12.1441631",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "b44be8260740460598816c40f13fd6b4",
|
||||
"PlanName": "测试12",
|
||||
"PlanCode": "8272fb",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 10:40:54.2543702",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "f189a50122d54a568f3d39dc1f996167",
|
||||
"PlanName": "0.5",
|
||||
"PlanCode": "2093ec",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 13:06:37.8280696",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "b48218c8f2274b108e278d019c9b5126",
|
||||
"PlanName": "3",
|
||||
"PlanCode": "9493bb",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 14:20:42.4761092",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "41d2ebc5ab5b4b2da3e203937c5cbe70",
|
||||
"PlanName": "6",
|
||||
"PlanCode": "5586de",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 15:21:03.4440875",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "49ec03499aa646b9b8069a783dbeca1c",
|
||||
"PlanName": "7",
|
||||
"PlanCode": "1162bc",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 15:31:33.7359724",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "a9c6d149cdf04636ac43cfb7623e4e7f",
|
||||
"PlanName": "8",
|
||||
"PlanCode": "7354eb",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 15:39:32.2399414",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "0e3a36cabefa4f5497e35193db48b559",
|
||||
"PlanName": "9",
|
||||
"PlanCode": "4453ba",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 15:49:31.5830134",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "d0a0d926e2034abc94b4d883951a78f7",
|
||||
"PlanName": "10",
|
||||
"PlanCode": "5797ab",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 16:00:25.4439315",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "22ac523a47e7421e80f401baf1526daf",
|
||||
"PlanName": "50",
|
||||
"PlanCode": "2507ca",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-16 16:23:13.8022807",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "fdea60f535ee4bc39c02c602a64f46bd",
|
||||
"PlanName": "11",
|
||||
"PlanCode": "1574ae",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 09:14:59.8230591",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "6650f7df6b8944f98476da92ce81d688",
|
||||
"PlanName": "12",
|
||||
"PlanCode": "2145bd",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 09:45:34.137906",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "9415a69280c042a09d6836f5eeddf40f",
|
||||
"PlanName": "100",
|
||||
"PlanCode": "2073fd",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 10:12:29.9998926",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "d9740fea94a04c2db44b1364a336b338",
|
||||
"PlanName": "250",
|
||||
"PlanCode": "2601ea",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 11:15:54.2583401",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "1d80c1fff5af442595c21963e6ca9fee",
|
||||
"PlanName": "160",
|
||||
"PlanCode": "6612ea",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 11:18:59.0457638",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "36889fb926aa480cb42de97700522bbf",
|
||||
"PlanName": "200",
|
||||
"PlanCode": "3174dc",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 11:20:15.7676326",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "bd90ae2846c14e708854938158fd3443",
|
||||
"PlanName": "300",
|
||||
"PlanCode": "2665df",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 13:00:16.9242256",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "9df4857d2bef45bcad14cc13055e9f7b",
|
||||
"PlanName": "500",
|
||||
"PlanCode": "4771ab",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 13:26:32.3910805",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "d2f6e63cf1ff41a4a8d03f4444a2aeac",
|
||||
"PlanName": "800",
|
||||
"PlanCode": "4560bc",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 13:42:35.5153947",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "f40a6f4326a346d39d5a82f6262aba47",
|
||||
"PlanName": "测试12345",
|
||||
"PlanCode": "3402ab",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 14:37:29.8890777",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "4248035f01e943faa6d71697ed386e19",
|
||||
"PlanName": "995",
|
||||
"PlanCode": "2688dc",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-18 14:39:23.5292196",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "a73bc780e4d04099bf54c2b90fa7b974",
|
||||
"PlanName": "1000",
|
||||
"PlanCode": "2889bf",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 09:16:37.7818522",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "4d97363a0a334094a1ff24494a902d02",
|
||||
"PlanName": "2.。",
|
||||
"PlanCode": "6527ff",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 11:38:00.0672017",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "6eec360c74464769967ebefa43b7aec1",
|
||||
"PlanName": "2222222",
|
||||
"PlanCode": "8763ce",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 11:40:42.7038484",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "986049c83b054171a1b34dd49b3ca9cf",
|
||||
"PlanName": "9ul",
|
||||
"PlanCode": "1945fd",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 13:33:06.6556398",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "462eed73962142c2bd3b8fe717caceb6",
|
||||
"PlanName": "8ul",
|
||||
"PlanCode": "6912fc",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 15:16:17.4254316",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "b2f0c7ab462f4cf1bae56ee59a49a253",
|
||||
"PlanName": "11.",
|
||||
"PlanCode": "6190ba",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 15:21:57.6729366",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "b9768a1d91444d4a86b7a013467bee95",
|
||||
"PlanName": "8ulll",
|
||||
"PlanCode": "6899be",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 15:29:03.2029069",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "98621898cd514bc9a1ac0c92362284f4",
|
||||
"PlanName": "7u",
|
||||
"PlanCode": "7651fe",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 15:57:16.4898686",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "4d03142fd86844db8e23c19061b3d505",
|
||||
"PlanName": "55555",
|
||||
"PlanCode": "7963fe",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 16:23:37.7271107",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "c78c3f38a59748c3aef949405e434b05",
|
||||
"PlanName": "44443",
|
||||
"PlanCode": "4564dd",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 16:29:26.6765074",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "0fc4ffd86091451db26162af4f7b235e",
|
||||
"PlanName": "u",
|
||||
"PlanCode": "9246de",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 16:34:15.4217796",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "a08748982b934daab8752f55796e1b0c",
|
||||
"PlanName": "666y",
|
||||
"PlanCode": "5492ce",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 16:38:55.6092122",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "2317611bdb614e45b61a5118e58e3a2a",
|
||||
"PlanName": "8ull、",
|
||||
"PlanCode": "4641de",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 16:46:26.6184295",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "62cb45ac3af64a46aa6d450ba56963e7",
|
||||
"PlanName": "33333",
|
||||
"PlanCode": "1270aa",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 16:49:19.6115492",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "321f717a3a2640a3bfc9515aee7d1052",
|
||||
"PlanName": "999",
|
||||
"PlanCode": "7597ed",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-01-19 16:58:22.6149002",
|
||||
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
|
||||
},
|
||||
{
|
||||
"uuid": "6c3246ac0f974a6abc24c83bf45e1cf4",
|
||||
"PlanName": "QPCR",
|
||||
"PlanCode": "7297ad",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-02-19 13:03:44.3456134",
|
||||
"MatrixId": "f02830f3-ed67-49fb-9865-c31828ba3a48"
|
||||
},
|
||||
{
|
||||
"uuid": "1d307a2c095b461abeec6e8521565ad3",
|
||||
"PlanName": "绝对定量",
|
||||
"PlanCode": "8540af",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-02-19 13:35:14.2243691",
|
||||
"MatrixId": "739ddf78-e04c-4d43-9293-c35d31f36f51"
|
||||
},
|
||||
{
|
||||
"uuid": "bbd6dc765867466ca2a415525f5bdbdd",
|
||||
"PlanName": "血凝",
|
||||
"PlanCode": "6513ee",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-02-20 16:14:25.0364174",
|
||||
"MatrixId": "20e70dcb-63f6-4bac-82e3-29e88eb6a7ab"
|
||||
},
|
||||
{
|
||||
"uuid": "f7282ecbfee44e91b05cefbc1beac1ae",
|
||||
"PlanName": "血凝抑制",
|
||||
"PlanCode": "1431ba",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-02-21 10:00:05.8661038",
|
||||
"MatrixId": "1c948beb-4c32-494f-b226-14bb84b3e144"
|
||||
},
|
||||
{
|
||||
"uuid": "196e0d757c574020932b64b69e88fac9",
|
||||
"PlanName": "测试杀杀杀",
|
||||
"PlanCode": "9833df",
|
||||
"PlanTarget": "",
|
||||
"Annotate": "",
|
||||
"CreateName": "",
|
||||
"CreateDate": "2024-02-21 10:54:19.3136491",
|
||||
"MatrixId": "3667ead7-9044-46ad-b73e-655b57c8c6b9"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,302 @@
|
||||
[
|
||||
{
|
||||
"id": "630a9ca9-dfbf-40f9-b90b-6df73e6a1d7f",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "db955443-1397-4a7a-a0cc-185eb6422c27",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "635e8265-e2b9-430e-8a4e-ddf94256266f",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 2,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "6de1521d-a249-4a7e-800f-1d49b5c7b56f",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "4f9f2527-0f71-4ec4-a0ac-e546407e2960",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "55ecff40-453f-4a5f-9ed3-1267b0a03cae",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "7dcd9c87-6702-4659-b28a-f6565b27f8e3",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "67e51bd6-6eee-46e4-931c-73d9e07397eb",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "e1289406-4f5e-4966-a1e6-fb29be6cd4bd",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 0,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "4ecb9ef7-cbd4-44bc-a6a9-fdbbefdc01d6",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "c7bcaeeb-7ce7-479d-8dae-e82f4023a2b6",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "e502d5ee-3197-4f60-8ac4-3bc005349dfd",
|
||||
"number": 7,
|
||||
"name": "T7",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "829c78b0-9e05-448f-9531-6d19c094c83f",
|
||||
"number": 8,
|
||||
"name": "T8",
|
||||
"row": 1,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "d0fd64d6-360d-4f5e-9451-21a332e247f5",
|
||||
"number": 9,
|
||||
"name": "T9",
|
||||
"row": 2,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "7f3da25d-0be0-4e07-885f-fbbbfa952f9f",
|
||||
"number": 10,
|
||||
"name": "T10",
|
||||
"row": 2,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "491d396d-7264-43d6-9ad4-60bffbe66c26",
|
||||
"number": 11,
|
||||
"name": "T11",
|
||||
"row": 2,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "a8853b6d-639d-46f9-a4bf-9153c0c22461",
|
||||
"number": 12,
|
||||
"name": "T12",
|
||||
"row": 2,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "b7beb8d0-0003-471d-bd8d-a9c0e09b07d5",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "306e3f96-a6d7-484a-83ef-722e3710d5c4",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "4e7bb617-ac1a-4360-b379-7ac4197089c4",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "af583180-c29d-418e-9061-9e030f77cf57",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 0,
|
||||
"col": 3,
|
||||
"row_span": 2,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "24a85ce8-e9e3-44f5-9d08-25116173ba75",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "7bf61a40-f65a-4d2f-bb19-d42bfd80e2e9",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "a3177806-3c02-4c4f-86d6-604a38c2ba2a",
|
||||
"number": 7,
|
||||
"name": "T7",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "8ccaad5a-8588-4ff3-b0d7-17e7fd5ac6cc",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "93ae7707-b6b8-4bc4-8700-c500c3d7b165",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "3591a07b-4922-4882-996f-7bebee843be1",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "669fdba9-b20c-4bd2-8352-8fe5682e3e0c",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "8bf3333e-4a73-4e4c-959a-8ae44e1038a2",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "2837bf69-273a-4cbb-a74c-0af1b362f609",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"uuid": "9a3007baa748457b8d5162f5c5918553",
|
||||
"ArmCode": "SC10",
|
||||
"ArmName": "单道-10uL",
|
||||
"CmdCode": "SC10",
|
||||
"ChannelNum": 1,
|
||||
"Dosage": 10,
|
||||
"CreateName": "admin",
|
||||
"CreateTime": "2021-11-13 14:04:02.000",
|
||||
"UpdateName": "admin",
|
||||
"UpdateTime": "2021-11-13 14:04:12.000"
|
||||
},
|
||||
{
|
||||
"uuid": "8f57a4cc859d4c02bffbeeadcfb2b661",
|
||||
"ArmCode": "SC300",
|
||||
"ArmName": "单道-300uL",
|
||||
"CmdCode": "SC300",
|
||||
"ChannelNum": 1,
|
||||
"Dosage": 300,
|
||||
"CreateName": "admin",
|
||||
"CreateTime": "2021-11-11 11:11:11.000",
|
||||
"UpdateName": "admin",
|
||||
"UpdateTime": "2021-11-11 11:11:11.000"
|
||||
},
|
||||
{
|
||||
"uuid": "8fe0320823de49a99bfa5060ce1aaa28",
|
||||
"ArmCode": "SC1250",
|
||||
"ArmName": "单道-1250",
|
||||
"CmdCode": "SC1250",
|
||||
"ChannelNum": 1,
|
||||
"Dosage": 1250,
|
||||
"CreateName": "admin",
|
||||
"CreateTime": "2021-11-12 10:10:10.000",
|
||||
"UpdateName": "admin",
|
||||
"UpdateTime": "2021-11-12 11:11:11.000"
|
||||
},
|
||||
{
|
||||
"uuid": "88f22c5384e94dbbad60961d4d2b5e91",
|
||||
"ArmCode": "MC10",
|
||||
"ArmName": "八道-10uL",
|
||||
"CmdCode": "MC10",
|
||||
"ChannelNum": 8,
|
||||
"Dosage": 10,
|
||||
"CreateName": "admin",
|
||||
"CreateTime": "2021-11-12 10:10:10.000",
|
||||
"UpdateName": "admin",
|
||||
"UpdateTime": "2021-11-13 12:12:12.000"
|
||||
},
|
||||
{
|
||||
"uuid": "09206ff90e64466f90ce6a785a24bad8",
|
||||
"ArmCode": "MC300",
|
||||
"ArmName": "八道-300uL",
|
||||
"CmdCode": "MC300",
|
||||
"ChannelNum": 8,
|
||||
"Dosage": 300,
|
||||
"CreateName": "admin",
|
||||
"CreateTime": "2021-11-12 12:12:12.000",
|
||||
"UpdateName": "admin",
|
||||
"UpdateTime": "2021-11-12 10:10:10.000"
|
||||
},
|
||||
{
|
||||
"uuid": "5afcbd7d1d6749079d1c94f8c2e68f06",
|
||||
"ArmCode": "MC1250",
|
||||
"ArmName": "八道-1250uL",
|
||||
"CmdCode": "MC1250",
|
||||
"ChannelNum": 8,
|
||||
"Dosage": 1250,
|
||||
"CreateName": "admin",
|
||||
"CreateTime": "2021-11-12 12:12:10.000",
|
||||
"UpdateName": "admin",
|
||||
"UpdateTime": "2021-11-12 12:11:11.000"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"uuid": "bd52d6566534441ea523265814dc06e8",
|
||||
"uuidMaterial": "01bdeb95a1314dc78b8f25667b08d531",
|
||||
"ChannelNum": 8,
|
||||
"HoleNo": 96,
|
||||
"HoleCenterXYZ": "300",
|
||||
"uuidLayoutMaster": "4f35adc958c540fcb40d6f9dd51e40fa"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"uuid": "4f35adc958c540fcb40d6f9dd51e40fa",
|
||||
"BoardCode": 34,
|
||||
"BoardNum": 1,
|
||||
"BoardLength": 500,
|
||||
"BoardWidth": 400,
|
||||
"BoardColum": 4,
|
||||
"BoardRow": 3,
|
||||
"TotalColum": 4,
|
||||
"TotalRow": 3,
|
||||
"BoardCenterXY": "300",
|
||||
"HoleQty": 96,
|
||||
"Version": 1,
|
||||
"CreateTime": "2021-11-15",
|
||||
"CreateName": "admin",
|
||||
"UpdateTime": "2021-11-15",
|
||||
"UpdateName": "admin"
|
||||
}
|
||||
]
|
||||
180578
unilabos/devices/liquid_handling/prcxi/json_output/base_plan_detail.json
Normal file
180578
unilabos/devices/liquid_handling/prcxi/json_output/base_plan_detail.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"id": "ef121889-2724-4b3d-a786-bbf0bd213c3d",
|
||||
"name": "9300_V02",
|
||||
"row": 2,
|
||||
"col": 3,
|
||||
"create_name": "",
|
||||
"create_time": "2023-08-12 16:02:20.994",
|
||||
"update_name": null,
|
||||
"update_time": null,
|
||||
"remark": "9300_V02",
|
||||
"isUse": 0
|
||||
},
|
||||
{
|
||||
"id": "9af15efc-29d2-4c44-8533-bbaf24913be6",
|
||||
"name": "9310",
|
||||
"row": 3,
|
||||
"col": 4,
|
||||
"create_name": "",
|
||||
"create_time": "2023-08-12 16:23:07.472",
|
||||
"update_name": null,
|
||||
"update_time": null,
|
||||
"remark": "9310",
|
||||
"isUse": 0
|
||||
},
|
||||
{
|
||||
"id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546",
|
||||
"name": "6版位",
|
||||
"row": 2,
|
||||
"col": 4,
|
||||
"create_name": "",
|
||||
"create_time": "2023-10-09 11:05:57.244",
|
||||
"update_name": null,
|
||||
"update_time": null,
|
||||
"remark": "6版位",
|
||||
"isUse": 0
|
||||
},
|
||||
{
|
||||
"id": "77673540-92c4-4404-b659-4257034a9c5e",
|
||||
"name": "9300_V03",
|
||||
"row": 2,
|
||||
"col": 3,
|
||||
"create_name": "",
|
||||
"create_time": "2024-01-20 08:49:09.620",
|
||||
"update_name": null,
|
||||
"update_time": null,
|
||||
"remark": "9300_V03",
|
||||
"isUse": 0
|
||||
},
|
||||
{
|
||||
"id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e",
|
||||
"name": "9320",
|
||||
"row": 4,
|
||||
"col": 7,
|
||||
"create_name": "",
|
||||
"create_time": "2025-03-10 13:44:17.994",
|
||||
"update_name": null,
|
||||
"update_time": null,
|
||||
"remark": "9320",
|
||||
"isUse": 0
|
||||
},
|
||||
{
|
||||
"id": "54092457-a8b8-4457-bccd-e8c251e83ebd",
|
||||
"name": "7.17演示",
|
||||
"row": 4,
|
||||
"col": 4,
|
||||
"create_name": "",
|
||||
"create_time": "2025-07-12 17:08:38.336",
|
||||
"update_name": null,
|
||||
"update_time": null,
|
||||
"remark": "7.17演示",
|
||||
"isUse": 0
|
||||
},
|
||||
{
|
||||
"id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc",
|
||||
"name": "北京大学 16版位",
|
||||
"row": 4,
|
||||
"col": 4,
|
||||
"create_name": "",
|
||||
"create_time": "2025-09-03 13:23:51.781",
|
||||
"update_name": null,
|
||||
"update_time": null,
|
||||
"remark": "北京大学 16版位",
|
||||
"isUse": 1
|
||||
},
|
||||
{
|
||||
"id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a",
|
||||
"name": "TEST",
|
||||
"row": 4,
|
||||
"col": 4,
|
||||
"create_name": "",
|
||||
"create_time": "2025-10-27 14:36:03.266",
|
||||
"update_name": null,
|
||||
"update_time": null,
|
||||
"remark": "TEST",
|
||||
"isUse": 0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,872 @@
|
||||
[
|
||||
{
|
||||
"id": "630a9ca9-dfbf-40f9-b90b-6df73e6a1d7f",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "db955443-1397-4a7a-a0cc-185eb6422c27",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "635e8265-e2b9-430e-8a4e-ddf94256266f",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 2,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "6de1521d-a249-4a7e-800f-1d49b5c7b56f",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "4f9f2527-0f71-4ec4-a0ac-e546407e2960",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
|
||||
},
|
||||
{
|
||||
"id": "55ecff40-453f-4a5f-9ed3-1267b0a03cae",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "7dcd9c87-6702-4659-b28a-f6565b27f8e3",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "67e51bd6-6eee-46e4-931c-73d9e07397eb",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "e1289406-4f5e-4966-a1e6-fb29be6cd4bd",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 0,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "4ecb9ef7-cbd4-44bc-a6a9-fdbbefdc01d6",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "c7bcaeeb-7ce7-479d-8dae-e82f4023a2b6",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "e502d5ee-3197-4f60-8ac4-3bc005349dfd",
|
||||
"number": 7,
|
||||
"name": "T7",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "829c78b0-9e05-448f-9531-6d19c094c83f",
|
||||
"number": 8,
|
||||
"name": "T8",
|
||||
"row": 1,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "d0fd64d6-360d-4f5e-9451-21a332e247f5",
|
||||
"number": 9,
|
||||
"name": "T9",
|
||||
"row": 2,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "7f3da25d-0be0-4e07-885f-fbbbfa952f9f",
|
||||
"number": 10,
|
||||
"name": "T10",
|
||||
"row": 2,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "491d396d-7264-43d6-9ad4-60bffbe66c26",
|
||||
"number": 11,
|
||||
"name": "T11",
|
||||
"row": 2,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "a8853b6d-639d-46f9-a4bf-9153c0c22461",
|
||||
"number": 12,
|
||||
"name": "T12",
|
||||
"row": 2,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
|
||||
},
|
||||
{
|
||||
"id": "b7beb8d0-0003-471d-bd8d-a9c0e09b07d5",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "306e3f96-a6d7-484a-83ef-722e3710d5c4",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "4e7bb617-ac1a-4360-b379-7ac4197089c4",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "af583180-c29d-418e-9061-9e030f77cf57",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 0,
|
||||
"col": 3,
|
||||
"row_span": 2,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "24a85ce8-e9e3-44f5-9d08-25116173ba75",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "7bf61a40-f65a-4d2f-bb19-d42bfd80e2e9",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "a3177806-3c02-4c4f-86d6-604a38c2ba2a",
|
||||
"number": 7,
|
||||
"name": "T7",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
|
||||
},
|
||||
{
|
||||
"id": "8ccaad5a-8588-4ff3-b0d7-17e7fd5ac6cc",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "93ae7707-b6b8-4bc4-8700-c500c3d7b165",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "3591a07b-4922-4882-996f-7bebee843be1",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "669fdba9-b20c-4bd2-8352-8fe5682e3e0c",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "8bf3333e-4a73-4e4c-959a-8ae44e1038a2",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "2837bf69-273a-4cbb-a74c-0af1b362f609",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
|
||||
},
|
||||
{
|
||||
"id": "e9d352fa-816a-4c01-a9e2-f52bce8771f1",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 4,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "713f1d85-b671-49f1-a2f9-11a64e5bb545",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 4,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "ba2d8fd6-e2fa-4dd3-8afc-13472ca12afb",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 4,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "68137a87-ae26-4e27-8953-4b1335ed957c",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 0,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "182b2814-9c89-4a75-8456-9a82e774f876",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 0,
|
||||
"col": 4,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "bc149d3c-9d54-45f0-8c33-23a5d4b70aff",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 0,
|
||||
"col": 5,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "7d9ce812-c39c-42fe-9b73-f35364a7b01f",
|
||||
"number": 7,
|
||||
"name": "T7",
|
||||
"row": 0,
|
||||
"col": 6,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "4907b17d-c3f8-40a6-a8a2-e874f66195b1",
|
||||
"number": 8,
|
||||
"name": "T8",
|
||||
"row": 1,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "f858fdb5-649f-4cb2-8e95-06a1b2d97113",
|
||||
"number": 9,
|
||||
"name": "T9",
|
||||
"row": 1,
|
||||
"col": 4,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "cc5f91d2-494a-4991-9dda-3b82ae61556b",
|
||||
"number": 10,
|
||||
"name": "T10",
|
||||
"row": 1,
|
||||
"col": 5,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "afed9a1f-2f48-4ca9-ae14-eb1ae4e80181",
|
||||
"number": 11,
|
||||
"name": "T11",
|
||||
"row": 1,
|
||||
"col": 6,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "1d39cacd-7828-4318-9d4f-5bf8fc21d77d",
|
||||
"number": 12,
|
||||
"name": "T12",
|
||||
"row": 2,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "086912ac-4f33-4214-a2c8-22acb5291bfe",
|
||||
"number": 13,
|
||||
"name": "T13",
|
||||
"row": 2,
|
||||
"col": 4,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "89d43ea4-93f6-4cbf-aba4-564b0067295f",
|
||||
"number": 14,
|
||||
"name": "T14",
|
||||
"row": 2,
|
||||
"col": 5,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "866b12a8-5ef6-426d-a65b-b0583a3d8f16",
|
||||
"number": 15,
|
||||
"name": "T15",
|
||||
"row": 2,
|
||||
"col": 6,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "6c5969a9-e763-48f4-97f4-a9027e3ea7ef",
|
||||
"number": 16,
|
||||
"name": "T16",
|
||||
"row": 3,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "af8370be-076d-455d-b0b3-dd246f76d930",
|
||||
"number": 17,
|
||||
"name": "T17",
|
||||
"row": 3,
|
||||
"col": 4,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "abf2b8c7-79ef-4fd1-9f9b-14e7e6a128c7",
|
||||
"number": 18,
|
||||
"name": "T18",
|
||||
"row": 3,
|
||||
"col": 5,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "ca92a1e9-eb7d-4f9a-a42c-9bae461da797",
|
||||
"number": 19,
|
||||
"name": "T19",
|
||||
"row": 3,
|
||||
"col": 6,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
|
||||
},
|
||||
{
|
||||
"id": "4a4df4fd-ea0b-461c-aad4-032bfda5abab",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 4,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "dba90870-4b7a-4fbd-b33f-948bbb594703",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "fddc5c2b-157f-4554-8b39-2c9e338f4d3a",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "2569a396-2cd8-4cac-8b78-a8af1313c993",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 0,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "f0f693c7-a45f-4dd3-b629-621461ca9992",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "9dcba2bf-8a48-4bc6-a9b1-88f51ffaa8af",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "08449a38-0dca-48c4-a156-6f1055cf74c4",
|
||||
"number": 7,
|
||||
"name": "T7",
|
||||
"row": 1,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "6ec7343f-12b9-42ae-86d1-3894758e69b4",
|
||||
"number": 8,
|
||||
"name": "T8",
|
||||
"row": 2,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "b5f02dbc-ffc6-452a-ad9f-2d1ff3db2064",
|
||||
"number": 9,
|
||||
"name": "T9",
|
||||
"row": 2,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "7635380a-4f96-4894-9a54-37c2bd27f148",
|
||||
"number": 10,
|
||||
"name": "T10",
|
||||
"row": 2,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "b4b6b063-5a0b-45a2-aa47-f427d4cd06f6",
|
||||
"number": 11,
|
||||
"name": "T11",
|
||||
"row": 3,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "af02c689-7bca-476b-bd05-ce21d3e83f27",
|
||||
"number": 12,
|
||||
"name": "T12",
|
||||
"row": 3,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "52a42e58-c0d6-420c-bc0b-575f749c7e3b",
|
||||
"number": 13,
|
||||
"name": "T13",
|
||||
"row": 3,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
|
||||
},
|
||||
{
|
||||
"id": "169c12fe-e2f4-465e-9fd3-e58eac83a502",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "b6072651-1df5-4946-a5b4-fbff3fa54e6a",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "d0b8ea7c-f06e-4d94-98a8-70ffcba73c47",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "a7a8eb69-63f6-494e-a441-b7aef0f7c8a4",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 0,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "21966669-6761-4e37-947c-12fec82173fb",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "2227b825-fe1d-4fa3-bcb2-6e4b3c10ea53",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "b799da88-c2d9-4ec4-81ec-bc0991a50fe5",
|
||||
"number": 7,
|
||||
"name": "T7",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "adaaa00a-ff6b-4bd8-b8f1-bb100488f306",
|
||||
"number": 8,
|
||||
"name": "T8",
|
||||
"row": 1,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "3bc98311-b548-46d3-a0e0-4f1edcf10e24",
|
||||
"number": 9,
|
||||
"name": "T9",
|
||||
"row": 2,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "81befc70-d249-49af-93dd-2efbe88c0211",
|
||||
"number": 10,
|
||||
"name": "T10",
|
||||
"row": 2,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "45dd5535-0293-4d27-beab-1e486657b148",
|
||||
"number": 11,
|
||||
"name": "T11",
|
||||
"row": 2,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "12ccf33a-6fe7-44a4-8643-b0b0ac6dd181",
|
||||
"number": 12,
|
||||
"name": "T12",
|
||||
"row": 2,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "900272dd-23fd-41a4-a366-254999a30487",
|
||||
"number": 13,
|
||||
"name": "T13",
|
||||
"row": 3,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "c366710d-2b81-4cee-8667-2b86e77e5c34",
|
||||
"number": 14,
|
||||
"name": "T14",
|
||||
"row": 3,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "e18a9271-bc66-4c2b-8bc1-0fb129b5cc2f",
|
||||
"number": 15,
|
||||
"name": "T15",
|
||||
"row": 3,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "6737cba0-de84-4c1f-992d-645e7f159b0c",
|
||||
"number": 16,
|
||||
"name": "T16",
|
||||
"row": 3,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
|
||||
},
|
||||
{
|
||||
"id": "8ace38ab-dbc7-48a1-8226-0fe92d176e07",
|
||||
"number": 1,
|
||||
"name": "T1",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
},
|
||||
{
|
||||
"id": "033fec53-c52d-4b59-aec6-2135ae0e18b9",
|
||||
"number": 2,
|
||||
"name": "T2",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
},
|
||||
{
|
||||
"id": "fa730930-8709-4250-928f-f757fce57b60",
|
||||
"number": 3,
|
||||
"name": "T3",
|
||||
"row": 0,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
},
|
||||
{
|
||||
"id": "e279d6f1-5243-4224-8953-1033dbea25ac",
|
||||
"number": 4,
|
||||
"name": "T4",
|
||||
"row": 0,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
},
|
||||
{
|
||||
"id": "76bd9426-6324-4af2-b12f-6ec0ff8c416e",
|
||||
"number": 5,
|
||||
"name": "T5",
|
||||
"row": 1,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
},
|
||||
{
|
||||
"id": "3f4ff652-3d87-4254-a235-bafde3359dae",
|
||||
"number": 6,
|
||||
"name": "T6",
|
||||
"row": 1,
|
||||
"col": 1,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
},
|
||||
{
|
||||
"id": "a38e94af-e91e-4e7a-b49d-8668001bb356",
|
||||
"number": 7,
|
||||
"name": "T7",
|
||||
"row": 1,
|
||||
"col": 2,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
},
|
||||
{
|
||||
"id": "9e45da24-1346-4886-a303-932880a79954",
|
||||
"number": 8,
|
||||
"name": "T8",
|
||||
"row": 1,
|
||||
"col": 3,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
},
|
||||
{
|
||||
"id": "1ac46e58-86ae-42d9-b230-d476b984507a",
|
||||
"number": 9,
|
||||
"name": "T9",
|
||||
"row": 2,
|
||||
"col": 0,
|
||||
"row_span": 1,
|
||||
"col_span": 1,
|
||||
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"uuid": "4034fa042e7f418db42ab80b0044a8cd",
|
||||
"Code": "MDHC-001-10",
|
||||
"Key": "c28ae2cb",
|
||||
"Value": "MDHC-001-1000522001001612db9dc",
|
||||
"CreateTime": "2022-01-22 17:07:00.8651386"
|
||||
},
|
||||
{
|
||||
"uuid": "8fb6d7589fdd42df93c1e1989ff13a62",
|
||||
"Code": "MDHC-001-10",
|
||||
"Key": "52980979",
|
||||
"Value": "MDHC-001-100052200100119bb6731",
|
||||
"CreateTime": "2022-01-22 20:19:20.9444209"
|
||||
},
|
||||
{
|
||||
"uuid": "efc4c92b40a94de6b0662c64486c18d1",
|
||||
"Code": "MDHC-001-10",
|
||||
"Key": "79da8402",
|
||||
"Value": "MDHC-001-1000522001001e24ea780",
|
||||
"CreateTime": "2022-01-22 20:19:26.8107506"
|
||||
},
|
||||
{
|
||||
"uuid": "3b81b1a9eabc4449b4dcbbbde47cb17f",
|
||||
"Code": "MDHC-001-10",
|
||||
"Key": "daa51755",
|
||||
"Value": "MDHC-001-100052200100185dd22e2",
|
||||
"CreateTime": "2022-01-22 20:19:36.1581374"
|
||||
},
|
||||
{
|
||||
"uuid": "d005a70801544e42ab9d216ad68dbf50",
|
||||
"Code": "MDHC-023-0.2",
|
||||
"Key": "992bbdab",
|
||||
"Value": "MDHC-023-0.2005220010014871a385",
|
||||
"CreateTime": "2022-02-16 15:49:53.760377"
|
||||
},
|
||||
{
|
||||
"uuid": "222315afb8e04320b0fcff10e3ddb8ae",
|
||||
"Code": "MDHC-023-0.2",
|
||||
"Key": "76d23270",
|
||||
"Value": "MDHC-023-0.200522001001e61547ee",
|
||||
"CreateTime": "2022-02-16 15:50:05.1932055"
|
||||
},
|
||||
{
|
||||
"uuid": "31e2a5d4f884419aa9ba96cef98b7385",
|
||||
"Code": "MDHC-023-0.2",
|
||||
"Key": "ba2b8a46",
|
||||
"Value": "MDHC-023-0.2005220010013bfed6cf",
|
||||
"CreateTime": "2022-02-16 17:26:20.0024235"
|
||||
},
|
||||
{
|
||||
"uuid": "9ccb8e0c5ca64ef09b8aced680395335",
|
||||
"Code": "MDHC-023-0.2",
|
||||
"Key": "1d1276d0",
|
||||
"Value": "MDHC-023-0.2005220010015c039a9c",
|
||||
"CreateTime": "2022-02-16 17:26:31.8479966"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"uuid": "f3932aeae93533f19c0519c4c14702aa",
|
||||
"RoleCode": "admin",
|
||||
"RoleName": "管理员",
|
||||
"RoleMenu": "all",
|
||||
"CreateTime": "2022-02-26 00:00:00.000",
|
||||
"CreateName": "admin",
|
||||
"UpdateTime": "2022-02-26 14:50:10.000",
|
||||
"UpdateName": "admin"
|
||||
},
|
||||
{
|
||||
"uuid": "8c822592b360345fb59690e49ac6b181",
|
||||
"RoleCode": "user",
|
||||
"RoleName": "实验员",
|
||||
"RoleMenu": "nosetting",
|
||||
"CreateTime": "2022-02-26 14:54:16.000",
|
||||
"CreateName": "admin",
|
||||
"UpdateTime": "2022-02-26 14:54:19.000",
|
||||
"UpdateName": "admin"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
[
|
||||
{
|
||||
"uuid": "f3932aeae93533f19c0519c4c14702dd",
|
||||
"UserName": "admin",
|
||||
"Password": "NuGlByx4NZBm7XcV9f89qA==",
|
||||
"RealName": "管理员",
|
||||
"IsEnable": 1,
|
||||
"uuidRole": "f3932aeae93533f19c0519c4c14702aa",
|
||||
"IsDel": 0,
|
||||
"CreateTime": "2022-02-26 14:51:41.000",
|
||||
"CreateName": "admin",
|
||||
"UpdateTime": "2022-02-26 14:51:49.000",
|
||||
"UpdateName": "admin"
|
||||
},
|
||||
{
|
||||
"uuid": "5c522592b366645fb55690e49ac6b166",
|
||||
"UserName": "user",
|
||||
"Password": "4QrcOUm6Wau+VuBX8g+IPg==",
|
||||
"RealName": "实验员",
|
||||
"IsEnable": 1,
|
||||
"uuidRole": "8c822592b360345fb59690e49ac6b181",
|
||||
"IsDel": 0,
|
||||
"CreateTime": "2022-02-26 14:56:57.000",
|
||||
"CreateName": "admin",
|
||||
"UpdateTime": "2022-02-26 14:58:39.000",
|
||||
"UpdateName": "admin"
|
||||
},
|
||||
{
|
||||
"uuid": "ju0514zjhi9267mz8s0buspq8b9s0bgb",
|
||||
"UserName": "Administrator",
|
||||
"Password": "3J17Il4KOR+wKPszf/0cHQ==",
|
||||
"RealName": "超级管理员",
|
||||
"IsEnable": 1,
|
||||
"uuidRole": "f3932aeae93533f19c0519c4c14702aa",
|
||||
"IsDel": 0,
|
||||
"CreateTime": "2023-08-12 00:00:00.000",
|
||||
"CreateName": "admin",
|
||||
"UpdateTime": "2023-08-12 00:00:00.000",
|
||||
"UpdateName": "admin"
|
||||
},
|
||||
{
|
||||
"uuid": "2",
|
||||
"UserName": "shortcut",
|
||||
"Password": "4QrcOUm6Wau+VuBX8g+IPg==",
|
||||
"RealName": "实验员",
|
||||
"IsEnable": 1,
|
||||
"uuidRole": "8c822592b360345fb59690e49ac6b181",
|
||||
"IsDel": 0,
|
||||
"CreateTime": null,
|
||||
"CreateName": "admin",
|
||||
"UpdateTime": "2023-10-23 00:00:00.000",
|
||||
"UpdateName": null
|
||||
}
|
||||
]
|
||||
@@ -70,50 +70,129 @@ class PRCXI9300Deck(Deck):
|
||||
super().__init__(name, size_x, size_y, size_z)
|
||||
self.slots = [None] * 6 # PRCXI 9300 有 6 个槽位
|
||||
|
||||
|
||||
class PRCXI9300Container(Plate, TipRack):
|
||||
"""PRCXI 9300 的专用 Container 类,继承自 Plate和TipRack。
|
||||
|
||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||
class PRCXI9300Plate(Plate):
|
||||
"""
|
||||
专用孔板类:
|
||||
1. 继承自 PLR 原生 Plate,保留所有物理特性。
|
||||
2. 增加 material_info 参数,用于在初始化时直接绑定 Unilab UUID。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size_x: float,
|
||||
size_y: float,
|
||||
size_z: float,
|
||||
category: str,
|
||||
ordering: collections.OrderedDict,
|
||||
model: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||
category: str = "plate",
|
||||
ordered_items: collections.OrderedDict = None,
|
||||
ordering: Optional[collections.OrderedDict] = None,
|
||||
model: Optional[str] = None,
|
||||
material_info: Optional[Dict[str, Any]] = None,
|
||||
**kwargs):
|
||||
items = ordered_items if ordered_items is not None else ordering
|
||||
super().__init__(name, size_x, size_y, size_z,
|
||||
ordered_items=items,
|
||||
category=category,
|
||||
model=model, **kwargs)
|
||||
|
||||
self._unilabos_state = {}
|
||||
if material_info:
|
||||
self._unilabos_state["Material"] = material_info
|
||||
|
||||
|
||||
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]]:
|
||||
try:
|
||||
data = super().serialize_state()
|
||||
except AttributeError:
|
||||
data = {}
|
||||
if hasattr(self, '_unilabos_state') and self._unilabos_state:
|
||||
safe_state = {}
|
||||
for k, v in self._unilabos_state.items():
|
||||
# 如果是 Material 字典,深入检查
|
||||
if k == "Material" and isinstance(v, dict):
|
||||
safe_material = {}
|
||||
for mk, mv in v.items():
|
||||
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
|
||||
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_material[mk] = mv
|
||||
else:
|
||||
# 打印日志提醒(可选)
|
||||
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
|
||||
pass
|
||||
safe_state[k] = safe_material
|
||||
# 其他顶层属性也进行类型检查
|
||||
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_state[k] = v
|
||||
|
||||
data.update(safe_state)
|
||||
return data
|
||||
|
||||
class PRCXI9300TipRack(TipRack):
|
||||
""" 专用吸头盒类 """
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||
category: str = "tip_rack",
|
||||
ordered_items: collections.OrderedDict = None,
|
||||
ordering: Optional[collections.OrderedDict] = None,
|
||||
model: Optional[str] = None,
|
||||
material_info: Optional[Dict[str, Any]] = None,
|
||||
**kwargs):
|
||||
items = ordered_items if ordered_items is not None else ordering
|
||||
super().__init__(name, size_x, size_y, size_z,
|
||||
ordered_items=items,
|
||||
category=category,
|
||||
model=model, **kwargs)
|
||||
self._unilabos_state = {}
|
||||
if material_info:
|
||||
self._unilabos_state["Material"] = material_info
|
||||
|
||||
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)
|
||||
try:
|
||||
data = super().serialize_state()
|
||||
except AttributeError:
|
||||
data = {}
|
||||
if hasattr(self, '_unilabos_state') and self._unilabos_state:
|
||||
safe_state = {}
|
||||
for k, v in self._unilabos_state.items():
|
||||
# 如果是 Material 字典,深入检查
|
||||
if k == "Material" and isinstance(v, dict):
|
||||
safe_material = {}
|
||||
for mk, mv in v.items():
|
||||
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
|
||||
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_material[mk] = mv
|
||||
else:
|
||||
# 打印日志提醒(可选)
|
||||
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
|
||||
pass
|
||||
safe_state[k] = safe_material
|
||||
# 其他顶层属性也进行类型检查
|
||||
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_state[k] = v
|
||||
|
||||
data.update(safe_state)
|
||||
return data
|
||||
|
||||
|
||||
|
||||
class PRCXI9300Trash(Trash):
|
||||
"""PRCXI 9300 的专用 Trash 类,继承自 Trash。
|
||||
|
||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, category: str, **kwargs):
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||
category: str = "trash",
|
||||
material_info: Optional[Dict[str, Any]] = None,
|
||||
**kwargs):
|
||||
|
||||
if name != "trash":
|
||||
name = "trash"
|
||||
print("PRCXI9300Trash name must be 'trash', using 'trash' instead.")
|
||||
super().__init__(name, size_x, size_y, size_z, category=category, **kwargs)
|
||||
print(f"Warning: PRCXI9300Trash usually expects name='trash' for backend logic, but got '{name}'.")
|
||||
super().__init__(name, size_x, size_y, size_z, **kwargs)
|
||||
self._unilabos_state = {}
|
||||
# 初始化时注入 UUID
|
||||
if material_info:
|
||||
self._unilabos_state["Material"] = material_info
|
||||
|
||||
def load_state(self, state: Dict[str, Any]) -> None:
|
||||
"""从给定的状态加载工作台信息。"""
|
||||
@@ -121,10 +200,152 @@ class PRCXI9300Trash(Trash):
|
||||
self._unilabos_state = state
|
||||
|
||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||
data = super().serialize_state()
|
||||
data.update(self._unilabos_state)
|
||||
try:
|
||||
data = super().serialize_state()
|
||||
except AttributeError:
|
||||
data = {}
|
||||
if hasattr(self, '_unilabos_state') and self._unilabos_state:
|
||||
safe_state = {}
|
||||
for k, v in self._unilabos_state.items():
|
||||
# 如果是 Material 字典,深入检查
|
||||
if k == "Material" and isinstance(v, dict):
|
||||
safe_material = {}
|
||||
for mk, mv in v.items():
|
||||
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
|
||||
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_material[mk] = mv
|
||||
else:
|
||||
# 打印日志提醒(可选)
|
||||
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
|
||||
pass
|
||||
safe_state[k] = safe_material
|
||||
# 其他顶层属性也进行类型检查
|
||||
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_state[k] = v
|
||||
|
||||
data.update(safe_state)
|
||||
return data
|
||||
|
||||
class PRCXI9300TubeRack(TubeRack):
|
||||
"""
|
||||
专用管架类:用于 EP 管架、试管架等。
|
||||
继承自 PLR 的 TubeRack,并支持注入 material_info (UUID)。
|
||||
"""
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||
category: str = "tube_rack",
|
||||
items: Optional[Dict[str, Any]] = None,
|
||||
ordered_items: Optional[OrderedDict] = None,
|
||||
model: Optional[str] = None,
|
||||
material_info: Optional[Dict[str, Any]] = None,
|
||||
**kwargs):
|
||||
|
||||
# 兼容处理:PLR 的 TubeRack 构造函数可能接受 items 或 ordered_items
|
||||
items_to_pass = items if items is not None else ordered_items
|
||||
super().__init__(name, size_x, size_y, size_z,
|
||||
ordered_items=ordered_items,
|
||||
model=model,
|
||||
**kwargs)
|
||||
|
||||
self._unilabos_state = {}
|
||||
if material_info:
|
||||
self._unilabos_state["Material"] = material_info
|
||||
|
||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||
try:
|
||||
data = super().serialize_state()
|
||||
except AttributeError:
|
||||
data = {}
|
||||
if hasattr(self, '_unilabos_state') and self._unilabos_state:
|
||||
safe_state = {}
|
||||
for k, v in self._unilabos_state.items():
|
||||
# 如果是 Material 字典,深入检查
|
||||
if k == "Material" and isinstance(v, dict):
|
||||
safe_material = {}
|
||||
for mk, mv in v.items():
|
||||
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
|
||||
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_material[mk] = mv
|
||||
else:
|
||||
# 打印日志提醒(可选)
|
||||
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
|
||||
pass
|
||||
safe_state[k] = safe_material
|
||||
# 其他顶层属性也进行类型检查
|
||||
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_state[k] = v
|
||||
|
||||
data.update(safe_state)
|
||||
return data
|
||||
|
||||
class PRCXI9300PlateAdapter(PlateAdapter):
|
||||
"""
|
||||
专用板式适配器类:用于承载 Plate 的底座(如 PCR 适配器、磁吸架等)。
|
||||
支持注入 material_info (UUID)。
|
||||
"""
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||
category: str = "plate_adapter",
|
||||
model: Optional[str] = None,
|
||||
material_info: Optional[Dict[str, Any]] = None,
|
||||
# 参数给予默认值 (标准96孔板尺寸)
|
||||
adapter_hole_size_x: float = 127.76,
|
||||
adapter_hole_size_y: float = 85.48,
|
||||
adapter_hole_size_z: float = 10.0, # 假设凹槽深度或板子放置高度
|
||||
dx: Optional[float] = None,
|
||||
dy: Optional[float] = None,
|
||||
dz: float = 0.0, # 默认Z轴偏移
|
||||
**kwargs):
|
||||
|
||||
# 自动居中计算:如果未指定 dx/dy,则根据适配器尺寸和孔尺寸计算居中位置
|
||||
if dx is None:
|
||||
dx = (size_x - adapter_hole_size_x) / 2
|
||||
if dy is None:
|
||||
dy = (size_y - adapter_hole_size_y) / 2
|
||||
|
||||
super().__init__(
|
||||
name=name,
|
||||
size_x=size_x,
|
||||
size_y=size_y,
|
||||
size_z=size_z,
|
||||
dx=dx,
|
||||
dy=dy,
|
||||
dz=dz,
|
||||
adapter_hole_size_x=adapter_hole_size_x,
|
||||
adapter_hole_size_y=adapter_hole_size_y,
|
||||
adapter_hole_size_z=adapter_hole_size_z,
|
||||
model=model,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self._unilabos_state = {}
|
||||
if material_info:
|
||||
self._unilabos_state["Material"] = material_info
|
||||
|
||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||
try:
|
||||
data = super().serialize_state()
|
||||
except AttributeError:
|
||||
data = {}
|
||||
if hasattr(self, '_unilabos_state') and self._unilabos_state:
|
||||
safe_state = {}
|
||||
for k, v in self._unilabos_state.items():
|
||||
# 如果是 Material 字典,深入检查
|
||||
if k == "Material" and isinstance(v, dict):
|
||||
safe_material = {}
|
||||
for mk, mv in v.items():
|
||||
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
|
||||
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_material[mk] = mv
|
||||
else:
|
||||
# 打印日志提醒(可选)
|
||||
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
|
||||
pass
|
||||
safe_state[k] = safe_material
|
||||
# 其他顶层属性也进行类型检查
|
||||
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
|
||||
safe_state[k] = v
|
||||
|
||||
data.update(safe_state)
|
||||
return data
|
||||
|
||||
class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
support_touch_tip = False
|
||||
@@ -978,7 +1199,30 @@ class PRCXI9300Api:
|
||||
|
||||
def _raw_request(self, payload: str) -> str:
|
||||
if self.debug:
|
||||
return " "
|
||||
# 调试/仿真模式下直接返回可解析的模拟 JSON,避免后续 json.loads 报错
|
||||
try:
|
||||
req = json.loads(payload)
|
||||
method = req.get("MethodName")
|
||||
except Exception:
|
||||
method = None
|
||||
|
||||
data: Any = True
|
||||
if method in {"AddSolution"}:
|
||||
data = str(uuid.uuid4())
|
||||
elif method in {"AddWorkTabletMatrix", "AddWorkTabletMatrix2"}:
|
||||
data = {"Success": True, "Message": "debug mock"}
|
||||
elif method in {"GetErrorCode"}:
|
||||
data = ""
|
||||
elif method in {"RemoveErrorCodet", "Reset", "Start", "LoadSolution", "Pause", "Resume", "Stop"}:
|
||||
data = True
|
||||
elif method in {"GetStepStateList", "GetStepStatus", "GetStepState"}:
|
||||
data = []
|
||||
elif method in {"GetLocation"}:
|
||||
data = {"X": 0, "Y": 0, "Z": 0}
|
||||
elif method in {"GetResetStatus"}:
|
||||
data = False
|
||||
|
||||
return json.dumps({"Success": True, "Msg": "debug mock", "Data": data})
|
||||
with contextlib.closing(socket.socket()) as sock:
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect((self.host, self.port))
|
||||
@@ -1702,31 +1946,31 @@ if __name__ == "__main__":
|
||||
from pylabrobot.resources.opentrons.tip_racks import tipone_96_tiprack_200ul, opentrons_96_tiprack_10ul
|
||||
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
|
||||
|
||||
def get_well_container(name: str) -> PRCXI9300Container:
|
||||
def get_well_container(name: str) -> PRCXI9300Plate:
|
||||
well_containers = corning_96_wellplate_360ul_flat(name).serialize()
|
||||
plate = PRCXI9300Container(
|
||||
name=name, size_x=50, size_y=50, size_z=10, category="plate", ordering=well_containers["ordering"]
|
||||
plate = PRCXI9300Plate(
|
||||
name=name, size_x=50, size_y=50, size_z=10, category="plate", ordered_items=well_containers["ordering"]
|
||||
)
|
||||
plate_serialized = plate.serialize()
|
||||
plate_serialized["parent_name"] = deck.name
|
||||
well_containers.update({k: v for k, v in plate_serialized.items() if k not in ["children"]})
|
||||
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
|
||||
new_plate: PRCXI9300Plate = PRCXI9300Plate.deserialize(well_containers)
|
||||
return new_plate
|
||||
|
||||
def get_tip_rack(name: str, child_prefix: str = "tip") -> PRCXI9300Container:
|
||||
def get_tip_rack(name: str, child_prefix: str = "tip") -> PRCXI9300TipRack:
|
||||
tip_racks = opentrons_96_tiprack_10ul(name).serialize()
|
||||
tip_rack = PRCXI9300Container(
|
||||
tip_rack = PRCXI9300TipRack(
|
||||
name=name,
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="tip_rack",
|
||||
ordering=collections.OrderedDict({k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()}),
|
||||
ordered_items=collections.OrderedDict({k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()}),
|
||||
)
|
||||
tip_rack_serialized = tip_rack.serialize()
|
||||
tip_rack_serialized["parent_name"] = deck.name
|
||||
tip_racks.update({k: v for k, v in tip_rack_serialized.items() if k not in ["children"]})
|
||||
new_tip_rack: PRCXI9300Container = PRCXI9300Container.deserialize(tip_racks)
|
||||
new_tip_rack: PRCXI9300TipRack = PRCXI9300TipRack.deserialize(tip_racks)
|
||||
return new_tip_rack
|
||||
|
||||
plate1 = get_tip_rack("RackT1")
|
||||
@@ -1773,8 +2017,8 @@ if __name__ == "__main__":
|
||||
}
|
||||
}
|
||||
)
|
||||
plate7 = PRCXI9300Container(
|
||||
name="plateT7", size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict()
|
||||
plate7 = PRCXI9300Plate(
|
||||
name="plateT7", size_x=50, size_y=50, size_z=10, category="plate", ordered_items=collections.OrderedDict()
|
||||
)
|
||||
plate7.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
|
||||
plate8 = get_tip_rack("PlateT8")
|
||||
@@ -1848,13 +2092,13 @@ if __name__ == "__main__":
|
||||
deck.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
|
||||
deck.assign_child_resource(plate2, location=Coordinate(0, 0, 0))
|
||||
deck.assign_child_resource(
|
||||
PRCXI9300Container(
|
||||
PRCXI9300Plate(
|
||||
name="container_for_nothin3",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="plate",
|
||||
ordering=collections.OrderedDict(),
|
||||
ordered_items=collections.OrderedDict(),
|
||||
),
|
||||
location=Coordinate(0, 0, 0),
|
||||
)
|
||||
@@ -1862,48 +2106,48 @@ if __name__ == "__main__":
|
||||
deck.assign_child_resource(plate5, location=Coordinate(0, 0, 0))
|
||||
deck.assign_child_resource(plate6, location=Coordinate(0, 0, 0))
|
||||
deck.assign_child_resource(
|
||||
PRCXI9300Container(
|
||||
PRCXI9300Plate(
|
||||
name="container_for_nothing7",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="plate",
|
||||
ordering=collections.OrderedDict(),
|
||||
ordered_items=collections.OrderedDict(),
|
||||
),
|
||||
location=Coordinate(0, 0, 0),
|
||||
)
|
||||
deck.assign_child_resource(
|
||||
PRCXI9300Container(
|
||||
PRCXI9300Plate(
|
||||
name="container_for_nothing8",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="plate",
|
||||
ordering=collections.OrderedDict(),
|
||||
ordered_items=collections.OrderedDict(),
|
||||
),
|
||||
location=Coordinate(0, 0, 0),
|
||||
)
|
||||
deck.assign_child_resource(plate9, location=Coordinate(0, 0, 0))
|
||||
deck.assign_child_resource(plate10, location=Coordinate(0, 0, 0))
|
||||
deck.assign_child_resource(
|
||||
PRCXI9300Container(
|
||||
PRCXI9300Plate(
|
||||
name="container_for_nothing11",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="plate",
|
||||
ordering=collections.OrderedDict(),
|
||||
ordered_items=collections.OrderedDict(),
|
||||
),
|
||||
location=Coordinate(0, 0, 0),
|
||||
)
|
||||
deck.assign_child_resource(
|
||||
PRCXI9300Container(
|
||||
PRCXI9300Plate(
|
||||
name="container_for_nothing12",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="plate",
|
||||
ordering=collections.OrderedDict(),
|
||||
ordered_items=collections.OrderedDict(),
|
||||
),
|
||||
location=Coordinate(0, 0, 0),
|
||||
)
|
||||
|
||||
841
unilabos/devices/liquid_handling/prcxi/prcxi_labware.py
Normal file
841
unilabos/devices/liquid_handling/prcxi/prcxi_labware.py
Normal file
@@ -0,0 +1,841 @@
|
||||
from typing import Optional
|
||||
from pylabrobot.resources import Tube, Coordinate
|
||||
from pylabrobot.resources.well import Well, WellBottomType, CrossSectionType
|
||||
from pylabrobot.resources.tip import Tip, TipCreator
|
||||
from pylabrobot.resources.tip_rack import TipRack, TipSpot
|
||||
from pylabrobot.resources.utils import create_ordered_items_2d
|
||||
from pylabrobot.resources.height_volume_functions import (
|
||||
compute_height_from_volume_rectangle,
|
||||
compute_volume_from_height_rectangle,
|
||||
)
|
||||
|
||||
from .prcxi import PRCXI9300Plate, PRCXI9300TipRack, PRCXI9300Trash, PRCXI9300TubeRack, PRCXI9300PlateAdapter
|
||||
|
||||
def _make_tip_helper(volume: float, length: float, depth: float) -> Tip:
|
||||
"""
|
||||
PLR 的 Tip 类参数名为: maximal_volume, total_tip_length, fitting_depth
|
||||
"""
|
||||
return Tip(
|
||||
has_filter=False, # 默认无滤芯
|
||||
maximal_volume=volume,
|
||||
total_tip_length=length,
|
||||
fitting_depth=depth
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# 标准品 参照 PLR 标准库的参数,但是用 PRCXI9300Plate 实例化,并注入 UUID
|
||||
# =========================================================================
|
||||
def PRCXI_BioER_96_wellplate(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: ZX-019-2.2 (2.2ml 深孔板)
|
||||
原型: pylabrobot.resources.bioer.BioER_96_wellplate_Vb_2200uL
|
||||
"""
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=127.1,
|
||||
size_y=85.0,
|
||||
size_z=44.2,
|
||||
lid=None,
|
||||
model="PRCXI_BioER_96_wellplate",
|
||||
category="plate",
|
||||
material_info={
|
||||
"uuid": "ca877b8b114a4310b429d1de4aae96ee",
|
||||
"Code": "ZX-019-2.2",
|
||||
"Name": "2.2ml 深孔板",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
},
|
||||
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
size_x=8.25,
|
||||
size_y=8.25,
|
||||
size_z=39.3, # 修改过
|
||||
dx=9.5,
|
||||
dy=7.5,
|
||||
dz=6,
|
||||
material_z_thickness=0.8,
|
||||
item_dx=9.0,
|
||||
item_dy=9.0,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
cross_section_type=CrossSectionType.RECTANGLE,
|
||||
bottom_type=WellBottomType.V, # 是否需要修改?
|
||||
max_volume=2200,
|
||||
),
|
||||
)
|
||||
def PRCXI_nest_1_troughplate(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: ZX-58-10000 (储液槽)
|
||||
原型: pylabrobot.resources.nest.nest_1_troughplate_195000uL_Vb
|
||||
"""
|
||||
well_size_x = 127.76 - (14.38 - 9 / 2) * 2
|
||||
well_size_y = 85.48 - (11.24 - 9 / 2) * 2
|
||||
well_kwargs = {
|
||||
"size_x": well_size_x,
|
||||
"size_y": well_size_y,
|
||||
"size_z": 26.85,
|
||||
"bottom_type": WellBottomType.V,
|
||||
"compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle(
|
||||
liquid_volume=liquid_volume, well_length=well_size_x, well_width=well_size_y
|
||||
),
|
||||
"compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle(
|
||||
liquid_height=liquid_height, well_length=well_size_x, well_width=well_size_y
|
||||
),
|
||||
"material_z_thickness": 31.4 - 26.85 - 3.55,
|
||||
}
|
||||
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=127.76,
|
||||
size_y=85.48,
|
||||
size_z=31.4,
|
||||
lid=None,
|
||||
model="PRCXI_Nest_1_troughplate",
|
||||
category="plate",
|
||||
material_info={
|
||||
"uuid": "04211a2dc93547fe9bf6121eac533650",
|
||||
"Code": "ZX-58-10000",
|
||||
"Name": "储液槽",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
},
|
||||
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=1,
|
||||
num_items_y=1,
|
||||
dx=14.38 - 9 / 2,
|
||||
dy=11.24 - 9 / 2,
|
||||
dz=3.55,
|
||||
item_dx=9.0,
|
||||
item_dy=9.0,
|
||||
**well_kwargs, # 传入上面计算好的孔参数
|
||||
),
|
||||
)
|
||||
def PRCXI_BioRad_384_wellplate(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: q3 (384板)
|
||||
原型: pylabrobot.resources.biorad.BioRad_384_wellplate_50uL_Vb
|
||||
"""
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
# 直接抄录 PLR 标准品的物理尺寸
|
||||
size_x=127.76,
|
||||
size_y=85.48,
|
||||
size_z=10.40,
|
||||
model="BioRad_384_wellplate_50uL_Vb",
|
||||
category="plate",
|
||||
# 2. 注入 Unilab 必须的 UUID 信息
|
||||
material_info={
|
||||
"uuid": "853dcfb6226f476e8b23c250217dc7da",
|
||||
"Code": "q3",
|
||||
"Name": "384板",
|
||||
"SupplyType": 1,
|
||||
},
|
||||
# 3. 定义孔的排列 (抄录标准参数)
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=24,
|
||||
num_items_y=16,
|
||||
dx=10.58, # A1 左边缘距离板子左边缘 需要进一步测量
|
||||
dy=7.44, # P1 下边缘距离板子下边缘 需要进一步测量
|
||||
dz=1.05,
|
||||
item_dx=4.5,
|
||||
item_dy=4.5,
|
||||
size_x=3.10,
|
||||
size_y=3.10,
|
||||
size_z=9.35,
|
||||
max_volume=50,
|
||||
material_z_thickness=1,
|
||||
bottom_type=WellBottomType.V,
|
||||
cross_section_type=CrossSectionType.CIRCLE,
|
||||
)
|
||||
)
|
||||
def PRCXI_AGenBio_4_troughplate(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: sdfrth654 (4道储液槽)
|
||||
原型: pylabrobot.resources.agenbio.AGenBio_4_troughplate_75000uL_Vb
|
||||
"""
|
||||
INNER_WELL_WIDTH = 26.1
|
||||
INNER_WELL_LENGTH = 71.2
|
||||
well_kwargs = {
|
||||
"size_x": 26,
|
||||
"size_y": 71.2,
|
||||
"size_z": 42.55,
|
||||
"bottom_type": WellBottomType.FLAT,
|
||||
"cross_section_type": CrossSectionType.RECTANGLE,
|
||||
"compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle(
|
||||
liquid_volume,
|
||||
INNER_WELL_LENGTH,
|
||||
INNER_WELL_WIDTH,
|
||||
),
|
||||
"compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle(
|
||||
liquid_height,
|
||||
INNER_WELL_LENGTH,
|
||||
INNER_WELL_WIDTH,
|
||||
),
|
||||
"material_z_thickness": 1,
|
||||
}
|
||||
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=127.76,
|
||||
size_y=85.48,
|
||||
size_z=43.80,
|
||||
model="PRCXI_AGenBio_4_troughplate",
|
||||
category="plate",
|
||||
material_info={
|
||||
"uuid": "01953864f6f140ccaa8ddffd4f3e46f5",
|
||||
"Code": "sdfrth654",
|
||||
"Name": "4道储液槽",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
},
|
||||
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=4,
|
||||
num_items_y=1,
|
||||
dx=9.8,
|
||||
dy=7.2,
|
||||
dz=0.9,
|
||||
item_dx=INNER_WELL_WIDTH + 1, # 1 mm wall thickness
|
||||
item_dy=INNER_WELL_LENGTH,
|
||||
**well_kwargs,
|
||||
),
|
||||
)
|
||||
def PRCXI_nest_12_troughplate(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: 12道储液槽 (12道储液槽)
|
||||
原型: pylabrobot.resources.nest.nest_12_troughplate_15000uL_Vb
|
||||
"""
|
||||
well_size_x = 8.2
|
||||
well_size_y = 71.2
|
||||
well_kwargs = {
|
||||
"size_x": well_size_x,
|
||||
"size_y": well_size_y,
|
||||
"size_z": 26.85,
|
||||
"bottom_type": WellBottomType.V,
|
||||
"compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle(
|
||||
liquid_volume=liquid_volume, well_length=well_size_x, well_width=well_size_y
|
||||
),
|
||||
"compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle(
|
||||
liquid_height=liquid_height, well_length=well_size_x, well_width=well_size_y
|
||||
),
|
||||
"material_z_thickness": 31.4 - 26.85 - 3.55,
|
||||
}
|
||||
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=127.76,
|
||||
size_y=85.48,
|
||||
size_z=31.4,
|
||||
lid=None,
|
||||
model="PRCXI_nest_12_troughplate",
|
||||
category="plate",
|
||||
material_info={
|
||||
"uuid": "0f1639987b154e1fac78f4fb29a1f7c1",
|
||||
"Code": "12道储液槽",
|
||||
"Name": "12道储液槽",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=12,
|
||||
num_items_y=1,
|
||||
dx=14.38 - 8.2 / 2,
|
||||
dy=(85.48 - 71.2) / 2,
|
||||
dz=3.55,
|
||||
item_dx=9.0,
|
||||
item_dy=9.0,
|
||||
**well_kwargs,
|
||||
),
|
||||
)
|
||||
def PRCXI_CellTreat_96_wellplate(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: ZX-78-096 (细菌培养皿)
|
||||
原型: pylabrobot.resources.celltreat.CellTreat_96_wellplate_350ul_Fb
|
||||
"""
|
||||
well_kwargs = {
|
||||
"size_x": 6.96,
|
||||
"size_y": 6.96,
|
||||
"size_z": 10.04,
|
||||
"bottom_type": WellBottomType.FLAT,
|
||||
"material_z_thickness": 1.75,
|
||||
"cross_section_type": CrossSectionType.CIRCLE,
|
||||
"max_volume": 300,
|
||||
}
|
||||
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=127.61,
|
||||
size_y=85.24,
|
||||
size_z=14.30,
|
||||
lid=None,
|
||||
model="PRCXI_CellTreat_96_wellplate",
|
||||
category="plate",
|
||||
material_info={
|
||||
"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f",
|
||||
"Code": "ZX-78-096",
|
||||
"Name": "细菌培养皿",
|
||||
"materialEnum": 4,
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=10.83,
|
||||
dy=7.67,
|
||||
dz=4.05,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
**well_kwargs,
|
||||
),
|
||||
)
|
||||
# =========================================================================
|
||||
# 自定义/需测量品 (Custom Measurement)
|
||||
# =========================================================================
|
||||
def PRCXI_10ul_eTips(name: str) -> PRCXI9300TipRack:
|
||||
"""
|
||||
对应 JSON Code: ZX-001-10+
|
||||
"""
|
||||
return PRCXI9300TipRack(
|
||||
name=name,
|
||||
size_x=122.11,
|
||||
size_y=85.48, #修改
|
||||
size_z=58.23,
|
||||
model="PRCXI_10ul_eTips",
|
||||
material_info={
|
||||
"uuid": "068b3815e36b4a72a59bae017011b29f",
|
||||
"Code": "ZX-001-10+",
|
||||
"Name": "10μL加长 Tip头",
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
TipSpot,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=7.97, #需要修改
|
||||
dy=5.0, #需修改
|
||||
dz=2.0, #需修改
|
||||
item_dx=9.0,
|
||||
item_dy=9.0,
|
||||
size_x=7.0,
|
||||
size_y=7.0,
|
||||
size_z=0,
|
||||
make_tip=lambda: _make_tip_helper(volume=10, length=52.0, depth=45.1)
|
||||
)
|
||||
)
|
||||
def PRCXI_300ul_Tips(name: str) -> PRCXI9300TipRack:
|
||||
"""
|
||||
对应 JSON Code: ZX-001-300
|
||||
吸头盒通常比较特殊,需要定义 Tip 对象
|
||||
"""
|
||||
return PRCXI9300TipRack(
|
||||
name=name,
|
||||
size_x=122.11,
|
||||
size_y=85.48, #修改
|
||||
size_z=58.23,
|
||||
model="PRCXI_300ul_Tips",
|
||||
material_info={
|
||||
"uuid": "076250742950465b9d6ea29a225dfb00",
|
||||
"Code": "ZX-001-300",
|
||||
"Name": "300μL Tip头",
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
TipSpot,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=7.97, #需要修改
|
||||
dy=5.0, #需修改
|
||||
dz=2.0, #需修改
|
||||
item_dx=9.0,
|
||||
item_dy=9.0,
|
||||
size_x=7.0,
|
||||
size_y=7.0,
|
||||
size_z=0,
|
||||
make_tip=lambda: _make_tip_helper(volume=300, length=60.0, depth=51.0)
|
||||
)
|
||||
)
|
||||
def PRCXI_PCR_Plate_200uL_nonskirted(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板)
|
||||
"""
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=119.5,
|
||||
size_y=80.0,
|
||||
size_z=26.0,
|
||||
model="PRCXI_PCR_Plate_200uL_nonskirted",
|
||||
plate_type="non-skirted",
|
||||
category="plate",
|
||||
material_info={
|
||||
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
|
||||
"Code": "ZX-023-0.2",
|
||||
"Name": "0.2ml PCR 板",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=7,
|
||||
dy=5,
|
||||
dz=0.0,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
size_x=6,
|
||||
size_y=6,
|
||||
size_z=15.17,
|
||||
bottom_type=WellBottomType.V,
|
||||
cross_section_type=CrossSectionType.CIRCLE,
|
||||
),
|
||||
)
|
||||
def PRCXI_PCR_Plate_200uL_semiskirted(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板)
|
||||
"""
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=126,
|
||||
size_y=86,
|
||||
size_z=21.2,
|
||||
model="PRCXI_PCR_Plate_200uL_semiskirted",
|
||||
plate_type="semi-skirted",
|
||||
category="plate",
|
||||
material_info={
|
||||
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
|
||||
"Code": "ZX-023-0.2",
|
||||
"Name": "0.2ml PCR 板",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=11,
|
||||
dy=8,
|
||||
dz=0.0,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
size_x=6,
|
||||
size_y=6,
|
||||
size_z=15.17,
|
||||
bottom_type=WellBottomType.V,
|
||||
cross_section_type=CrossSectionType.CIRCLE,
|
||||
),
|
||||
)
|
||||
def PRCXI_PCR_Plate_200uL_skirted(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板)
|
||||
"""
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=127.76,
|
||||
size_y=86,
|
||||
size_z=16.1,
|
||||
model="PRCXI_PCR_Plate_200uL_skirted",
|
||||
plate_type="skirted",
|
||||
category="plate",
|
||||
material_info={
|
||||
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
|
||||
"Code": "ZX-023-0.2",
|
||||
"Name": "0.2ml PCR 板",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=11,
|
||||
dy=8.49,
|
||||
dz=0.8,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
size_x=6,
|
||||
size_y=6,
|
||||
size_z=15.1,
|
||||
bottom_type=WellBottomType.V,
|
||||
cross_section_type=CrossSectionType.CIRCLE,
|
||||
),
|
||||
)
|
||||
def PRCXI_trash(name: str = "trash") -> PRCXI9300Trash:
|
||||
"""
|
||||
对应 JSON Code: q1 (废弃槽)
|
||||
"""
|
||||
return PRCXI9300Trash(
|
||||
name="trash",
|
||||
size_x=126.59,
|
||||
size_y=84.87,
|
||||
size_z=89.5, # 修改
|
||||
category="trash",
|
||||
model="PRCXI_trash",
|
||||
material_info={
|
||||
"uuid": "730067cf07ae43849ddf4034299030e9",
|
||||
"Code": "q1",
|
||||
"Name": "废弃槽",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
}
|
||||
)
|
||||
def PRCXI_96_DeepWell(name: str) -> PRCXI9300Plate:
|
||||
"""
|
||||
对应 JSON Code: q2 (96深孔板)
|
||||
"""
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=127.3,
|
||||
size_y=85.35,
|
||||
size_z=45.0, #修改
|
||||
model="PRCXI_96_DeepWell",
|
||||
material_info={
|
||||
"uuid": "57b1e4711e9e4a32b529f3132fc5931f", # 对应 q2 uuid
|
||||
"Code": "q2",
|
||||
"Name": "96深孔板",
|
||||
"materialEnum": 0
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=10.9,
|
||||
dy=8.25,
|
||||
dz=2.0,
|
||||
item_dx=9.0,
|
||||
item_dy=9.0,
|
||||
size_x=8.2,
|
||||
size_y=8.2,
|
||||
size_z=42.0,
|
||||
max_volume=2200
|
||||
)
|
||||
)
|
||||
def PRCXI_EP_Adapter(name: str) -> PRCXI9300TubeRack:
|
||||
"""
|
||||
对应 JSON Code: 1 (ep适配器)
|
||||
这是一个 4x6 的 EP 管架,适配 1.5mL/2.0mL 离心管
|
||||
"""
|
||||
ep_tube_prototype = Tube(
|
||||
name="EP_Tube_1.5mL",
|
||||
size_x=10.6,
|
||||
size_y=10.6,
|
||||
size_z=40.0, # 管子本身的高度,通常比架子孔略高或持平
|
||||
max_volume=1500,
|
||||
model="EP_Tube_1.5mL"
|
||||
)
|
||||
|
||||
# 计算 PRCXI9300TubeRack 中孔的起始位置 dx, dy
|
||||
dy_calc = 85.8 - 10.5 - (3 * 18) - 10.6
|
||||
dx_calc = 3.54
|
||||
return PRCXI9300TubeRack(
|
||||
name=name,
|
||||
size_x=128.04,
|
||||
size_y=85.8,
|
||||
size_z=42.66,
|
||||
model="PRCXI_EP_Adapter",
|
||||
category="tube_rack",
|
||||
material_info={
|
||||
"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7",
|
||||
"Code": "1",
|
||||
"Name": "ep适配器",
|
||||
"materialEnum": 0,
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Tube,
|
||||
num_items_x=6,
|
||||
num_items_y=4,
|
||||
dx=dx_calc,
|
||||
dy=dy_calc,
|
||||
dz=42.66 - 38.08, # 架高 - 孔深
|
||||
item_dx=21.0,
|
||||
item_dy=18.0,
|
||||
size_x=10.6,
|
||||
size_y=10.6,
|
||||
size_z=40.0,
|
||||
max_volume=1500
|
||||
)
|
||||
)
|
||||
# =========================================================================
|
||||
# 无实物,需要测量
|
||||
# =========================================================================
|
||||
def PRCXI_Tip1250_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
""" Code: ZX-58-1250 """
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=128,
|
||||
size_y=85,
|
||||
size_z=20,
|
||||
material_info={
|
||||
"uuid": "3b6f33ffbf734014bcc20e3c63e124d4",
|
||||
"Code": "ZX-58-1250",
|
||||
"Name": "Tip头适配器 1250uL",
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
def PRCXI_Tip300_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
""" Code: ZX-58-300 """
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=127,
|
||||
size_y=85,
|
||||
size_z=81,
|
||||
material_info={
|
||||
"uuid": "7c822592b360451fb59690e49ac6b181",
|
||||
"Code": "ZX-58-300",
|
||||
"Name": "ZHONGXI 适配器 300uL",
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
def PRCXI_Tip10_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
""" Code: ZX-58-10 """
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=128,
|
||||
size_y=85,
|
||||
size_z=72.3,
|
||||
material_info={
|
||||
"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c",
|
||||
"Code": "ZX-58-10",
|
||||
"Name": "吸头10ul 适配器",
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
def PRCXI_1250uL_Tips(name: str) -> PRCXI9300TipRack:
|
||||
""" Code: ZX-001-1250 """
|
||||
return PRCXI9300TipRack(
|
||||
name=name,
|
||||
size_x=118.09,
|
||||
size_y=80.7,
|
||||
size_z=107.67,
|
||||
model="PRCXI_1250uL_Tips",
|
||||
material_info={
|
||||
"uuid": "7960f49ddfe9448abadda89bd1556936",
|
||||
"Code": "ZX-001-1250",
|
||||
"Name": "1250μL Tip头",
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
TipSpot,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=9.545 - 7.95/2,
|
||||
dy=8.85 - 7.95/2,
|
||||
dz=2.0,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
size_x=7.0,
|
||||
size_y=7.0,
|
||||
size_z=0,
|
||||
make_tip=lambda: _make_tip_helper(volume=1250, length=107.67, depth=8)
|
||||
)
|
||||
)
|
||||
def PRCXI_10uL_Tips(name: str) -> PRCXI9300TipRack:
|
||||
""" Code: ZX-001-10 """
|
||||
return PRCXI9300TipRack(
|
||||
name=name,
|
||||
size_x=120.98,
|
||||
size_y=82.12,
|
||||
size_z=67,
|
||||
model="PRCXI_10uL_Tips",
|
||||
material_info={
|
||||
"uuid": "45f2ed3ad925484d96463d675a0ebf66",
|
||||
"Code": "ZX-001-10",
|
||||
"Name": "10μL Tip头",
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
TipSpot,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=10.99 - 5/2,
|
||||
dy=9.56 - 5/2,
|
||||
dz=2.0,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
size_x=7.0,
|
||||
size_y=7.0,
|
||||
size_z=0,
|
||||
make_tip=lambda: _make_tip_helper(volume=1250, length=52.0, depth=5)
|
||||
)
|
||||
)
|
||||
def PRCXI_1000uL_Tips(name: str) -> PRCXI9300TipRack:
|
||||
""" Code: ZX-001-1000 """
|
||||
return PRCXI9300TipRack(
|
||||
name=name,
|
||||
size_x=128.09,
|
||||
size_y=85.8,
|
||||
size_z=98,
|
||||
model="PRCXI_1000uL_Tips",
|
||||
material_info={
|
||||
"uuid": "80652665f6a54402b2408d50b40398df",
|
||||
"Code": "ZX-001-1000",
|
||||
"Name": "1000μL Tip头",
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
TipSpot,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=14.5 - 7.95/2,
|
||||
dy=7.425,
|
||||
dz=2.0,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
size_x=7.0,
|
||||
size_y=7.0,
|
||||
size_z=0,
|
||||
make_tip=lambda: _make_tip_helper(volume=1000, length=55.0, depth=8)
|
||||
)
|
||||
)
|
||||
def PRCXI_200uL_Tips(name: str) -> PRCXI9300TipRack:
|
||||
""" Code: ZX-001-200 """
|
||||
return PRCXI9300TipRack(
|
||||
name=name,
|
||||
size_x=120.98,
|
||||
size_y=82.12,
|
||||
size_z=66.9,
|
||||
model="PRCXI_200uL_Tips",
|
||||
material_info={
|
||||
"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7",
|
||||
"Code": "ZX-001-200",
|
||||
"Name": "200μL Tip头",
|
||||
"SupplyType": 1},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
TipSpot,
|
||||
num_items_x=12,
|
||||
num_items_y=8,
|
||||
dx=10.99 - 5.5/2,
|
||||
dy=9.56 - 5.5/2,
|
||||
dz=2.0,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
size_x=7.0,
|
||||
size_z=0,
|
||||
size_y=7.0,
|
||||
make_tip=lambda: _make_tip_helper(volume=200, length=52.0, depth=5)
|
||||
)
|
||||
)
|
||||
def PRCXI_PCR_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
"""
|
||||
对应 JSON Code: ZX-58-0001 (全裙边 PCR适配器)
|
||||
"""
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=127.76,
|
||||
size_y=85.48,
|
||||
size_z=21.69,
|
||||
model="PRCXI_PCR_Adapter",
|
||||
material_info={
|
||||
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
|
||||
"Code": "ZX-58-0001",
|
||||
"Name": "全裙边 PCR适配器",
|
||||
"materialEnum": 3,
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
def PRCXI_Reservoir_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
""" Code: ZX-ADP-001 """
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=133,
|
||||
size_y=91.8,
|
||||
size_z=70,
|
||||
material_info={
|
||||
"uuid": "6bdfdd7069df453896b0806df50f2f4d",
|
||||
"Code": "ZX-ADP-001",
|
||||
"Name": "储液槽 适配器",
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
def PRCXI_Deep300_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
""" Code: ZX-002-300 """
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=136.4,
|
||||
size_y=93.8,
|
||||
size_z=96,
|
||||
material_info={
|
||||
"uuid": "9a439bed8f3344549643d6b3bc5a5eb4",
|
||||
"Code": "ZX-002-300",
|
||||
"Name": "300ul深孔板适配器",
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
def PRCXI_Deep10_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
""" Code: ZX-002-10 """
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=136.5,
|
||||
size_y=93.8,
|
||||
size_z=121.5,
|
||||
material_info={
|
||||
"uuid": "4dc8d6ecfd0449549683b8ef815a861b",
|
||||
"Code": "ZX-002-10",
|
||||
"Name": "10ul专用深孔板适配器",
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
def PRCXI_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
""" Code: Fhh478 """
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=120,
|
||||
size_y=90,
|
||||
size_z=86,
|
||||
material_info={
|
||||
"uuid": "adfabfffa8f24af5abfbba67b8d0f973",
|
||||
"Code": "Fhh478",
|
||||
"Name": "适配器",
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
def PRCXI_48_DeepWell(name: str) -> PRCXI9300Plate:
|
||||
""" Code: 22 (48孔深孔板) """
|
||||
print("Warning: Code '22' (48孔深孔板) dimensions are null in JSON.")
|
||||
return PRCXI9300Plate(
|
||||
name=name,
|
||||
size_x=127,
|
||||
size_y=85,
|
||||
size_z=44,
|
||||
model="PRCXI_48_DeepWell",
|
||||
material_info={
|
||||
"uuid": "026c5d5cf3d94e56b4e16b7fb53a995b",
|
||||
"Code": "22",
|
||||
"Name": "48孔深孔板",
|
||||
"SupplyType": 1
|
||||
},
|
||||
ordered_items=create_ordered_items_2d(
|
||||
Well,
|
||||
num_items_x=6,
|
||||
num_items_y=8,
|
||||
dx=10,
|
||||
dy=10,
|
||||
dz=1,
|
||||
item_dx=18.5,
|
||||
item_dy=9,
|
||||
size_x=8,
|
||||
size_y=8,
|
||||
size_z=40
|
||||
)
|
||||
)
|
||||
def PRCXI_30mm_Adapter(name: str) -> PRCXI9300PlateAdapter:
|
||||
""" Code: ZX-58-30 """
|
||||
return PRCXI9300PlateAdapter(
|
||||
name=name,
|
||||
size_x=132,
|
||||
size_y=93.5,
|
||||
size_z=30,
|
||||
material_info={
|
||||
"uuid": "a0757a90d8e44e81a68f306a608694f2",
|
||||
"Code": "ZX-58-30",
|
||||
"Name": "30mm适配器",
|
||||
"SupplyType": 2
|
||||
}
|
||||
)
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"Tip头适配器 1250uL": {"uuid": "3b6f33ffbf734014bcc20e3c63e124d4", "materialEnum": "0"},
|
||||
"ZHONGXI 适配器 300uL": {"uuid": "7c822592b360451fb59690e49ac6b181", "materialEnum": "0"},
|
||||
"吸头10ul 适配器": {"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c", "materialEnum": "0"},
|
||||
"1250μL Tip头": {"uuid": "7960f49ddfe9448abadda89bd1556936", "materialEnum": "0"},
|
||||
"10μL Tip头": {"uuid": "45f2ed3ad925484d96463d675a0ebf66", "materialEnum": "0"},
|
||||
"10μL加长 Tip头": {"uuid": "068b3815e36b4a72a59bae017011b29f", "materialEnum": "1"},
|
||||
"1000μL Tip头": {"uuid": "80652665f6a54402b2408d50b40398df", "materialEnum": "1"},
|
||||
"300μL Tip头": {"uuid": "076250742950465b9d6ea29a225dfb00", "materialEnum": "1"},
|
||||
"200μL Tip头": {"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7", "materialEnum": "0"},
|
||||
"0.2ml PCR板": {"uuid": "73bb9b10bc394978b70e027bf45ce2d3", "materialEnum": "0"},
|
||||
"2.2ml 深孔板": {"uuid": "ca877b8b114a4310b429d1de4aae96ee", "materialEnum": "0"},
|
||||
"储液槽": {"uuid": "04211a2dc93547fe9bf6121eac533650", "materialEnum": "0"},
|
||||
"全裙边 PCR适配器": {"uuid": "4a043a07c65a4f9bb97745e1f129b165", "materialEnum": "3"},
|
||||
"储液槽 适配器": {"uuid": "6bdfdd7069df453896b0806df50f2f4d", "materialEnum": "0"},
|
||||
"300ul深孔板适配器": {"uuid": "9a439bed8f3344549643d6b3bc5a5eb4", "materialEnum": "0"},
|
||||
"10ul专用深孔板适配器": {"uuid": "4dc8d6ecfd0449549683b8ef815a861b", "materialEnum": "0"},
|
||||
"爱津": {"uuid": "b01627718d3341aba649baa81c2c083c", "materialEnum": "0"},
|
||||
"适配器": {"uuid": "adfabfffa8f24af5abfbba67b8d0f973", "materialEnum": "0"},
|
||||
"废弃槽": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": "0"},
|
||||
"96深孔板": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": "0"},
|
||||
"384板": {"uuid": "853dcfb6226f476e8b23c250217dc7da", "materialEnum": "0"},
|
||||
"4道储液槽": {"uuid": "01953864f6f140ccaa8ddffd4f3e46f5", "materialEnum": "0"},
|
||||
"48孔深孔板": {"uuid": "026c5d5cf3d94e56b4e16b7fb53a995b", "materialEnum": "2"},
|
||||
"12道储液槽": {"uuid": "0f1639987b154e1fac78f4fb29a1f7c1", "materialEnum": "0"},
|
||||
"HPLC料盘": {"uuid": "548bbc3df0d4447586f2c19d2c0c0c55", "materialEnum": "0"},
|
||||
"ep适配器": {"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7", "materialEnum": "0"},
|
||||
"30mm适配器": {"uuid": "a0757a90d8e44e81a68f306a608694f2", "materialEnum": "0"},
|
||||
"细菌培养皿": {"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f", "materialEnum": "4"},
|
||||
"96 细胞培养皿":{ "uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": "0"}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import collections
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container
|
||||
|
||||
|
||||
prcxi_materials_path = str(Path(__file__).parent / "prcxi_material.json")
|
||||
with open(prcxi_materials_path, mode="r", encoding="utf-8") as f:
|
||||
prcxi_materials = json.loads(f.read())
|
||||
|
||||
|
||||
def tip_adaptor_1250ul(name="Tip头适配器 1250uL") -> PRCXI9300Container: # 必须传入一个name参数,是plr的规范要求
|
||||
# tip_rack = PRCXI9300Container(name, prcxi_materials["name"]["Height"])
|
||||
tip_rack = PRCXI9300Container(name, 1000,400,800, "tip_rack", collections.OrderedDict())
|
||||
tip_rack.load_state({
|
||||
"Materials": {"uuid": "7960f49ddfe9448abadda89bd1556936", "materialEnum": "0"}
|
||||
})
|
||||
return tip_rack
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import collections
|
||||
|
||||
from pylabrobot.resources import opentrons_96_tiprack_10ul
|
||||
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
|
||||
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container, PRCXI9300Trash
|
||||
|
||||
|
||||
def get_well_container(name: str) -> PRCXI9300Container:
|
||||
well_containers = corning_96_wellplate_360ul_flat(name).serialize()
|
||||
plate = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="plate",
|
||||
ordering=collections.OrderedDict())
|
||||
plate_serialized = plate.serialize()
|
||||
well_containers.update({k: v for k, v in plate_serialized.items() if k not in ["children"]})
|
||||
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
|
||||
return new_plate
|
||||
|
||||
def get_tip_rack(name: str) -> PRCXI9300Container:
|
||||
tip_racks = opentrons_96_tiprack_10ul("name").serialize()
|
||||
tip_rack = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="tip_rack",
|
||||
ordering=collections.OrderedDict())
|
||||
tip_rack_serialized = tip_rack.serialize()
|
||||
tip_racks.update({k: v for k, v in tip_rack_serialized.items() if k not in ["children"]})
|
||||
new_tip_rack: PRCXI9300Container = PRCXI9300Container.deserialize(tip_racks)
|
||||
return new_tip_rack
|
||||
|
||||
def prcxi_96_wellplate_360ul_flat(name: str):
|
||||
return get_well_container(name)
|
||||
|
||||
def prcxi_opentrons_96_tiprack_10ul(name: str):
|
||||
return get_tip_rack(name)
|
||||
|
||||
def prcxi_trash(name: str = None):
|
||||
return PRCXI9300Trash(name="trash", size_x=50, size_y=50, size_z=10, category="trash")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
test_plate = prcxi_96_wellplate_360ul_flat("test_plate")
|
||||
test_rack = prcxi_opentrons_96_tiprack_10ul("test_rack")
|
||||
tash = prcxi_trash("trash")
|
||||
print(test_plate)
|
||||
print(test_rack)
|
||||
print(tash)
|
||||
# Output will be a dictionary representation of the PRCXI9300Container with well details
|
||||
@@ -0,0 +1,560 @@
|
||||
# 新威电池测试系统 - OSS 上传功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
本次更新为新威电池测试系统添加了**阿里云 OSS 文件上传功能**,采用统一的 API 方式,允许将测试数据备份文件上传到云端存储。
|
||||
|
||||
## 版本更新说明
|
||||
|
||||
### ⚠️ 重大变更(2025-12-17)
|
||||
|
||||
本次更新将 OSS 上传方式从 **`oss2` 库** 改为 **统一 API 方式**,实现与团队其他系统的统一。
|
||||
|
||||
**主要变化**:
|
||||
- ✅ 用 `requests` 库
|
||||
- ✅ 通过统一 API 获取预签名 URL 进行上传
|
||||
- ✅ 简化环境变量配置(仅需要 JWT Token)
|
||||
- ✅ 返回文件访问 URL
|
||||
|
||||
## 主要改动
|
||||
|
||||
### 1. OSS 上传工具函数重构(第30-200行)
|
||||
|
||||
#### 新增函数
|
||||
|
||||
- **`get_upload_token(base_url, auth_token, scene, filename)`**
|
||||
从统一 API 获取文件上传的预签名 URL
|
||||
|
||||
- **`upload_file_with_presigned_url(upload_info, file_path)`**
|
||||
使用预签名 URL 上传文件到 OSS
|
||||
|
||||
#### 更新的函数
|
||||
|
||||
- **`upload_file_to_oss(local_file_path, oss_object_name)`**
|
||||
上传单个文件到阿里云 OSS(使用统一 API 方式)
|
||||
- 返回值变更:成功时返回文件访问 URL,失败时返回 `False`
|
||||
|
||||
- **`upload_files_to_oss(file_paths, oss_prefix)`**
|
||||
批量上传文件列表
|
||||
- `oss_prefix` 参数保留但暂不使用(接口兼容性)
|
||||
|
||||
- **`upload_directory_to_oss(local_dir, oss_prefix)`**
|
||||
上传整个目录
|
||||
- 简化实现,直接使用文件名上传
|
||||
|
||||
### 2. 环境变量配置简化
|
||||
|
||||
#### 新方式(推荐)
|
||||
```bash
|
||||
# ✅ 必需
|
||||
UNI_LAB_AUTH_TOKEN # API Key 格式: "Api xxxxxx"
|
||||
|
||||
# ✅ 可选(有默认值)
|
||||
UNI_LAB_BASE_URL (默认: https://uni-lab.test.bohrium.com)
|
||||
UNI_LAB_UPLOAD_SCENE (默认: job,其他值会被改成 default)
|
||||
```
|
||||
|
||||
### 3. 初始化方法(保持不变)
|
||||
|
||||
`__init__` 方法中的 OSS 相关配置参数:
|
||||
|
||||
```python
|
||||
# OSS 上传配置
|
||||
self.oss_upload_enabled = False # 默认不启用 OSS 上传
|
||||
self.oss_prefix = "neware_backup" # OSS 对象路径前缀
|
||||
self._last_backup_dir = None # 记录最近一次的 backup_dir
|
||||
```
|
||||
|
||||
**默认行为**:OSS 上传功能默认关闭(`oss_upload_enabled=False`),不影响现有系统。
|
||||
|
||||
### 4. upload_backup_to_oss 方法(保持不变)
|
||||
|
||||
```python
|
||||
def upload_backup_to_oss(
|
||||
self,
|
||||
backup_dir: str = None,
|
||||
file_pattern: str = "*",
|
||||
oss_prefix: str = None
|
||||
) -> dict
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 前置条件
|
||||
|
||||
#### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
# requests 库(通常已安装)
|
||||
pip install requests
|
||||
```
|
||||
|
||||
#### 2. 配置环境变量
|
||||
|
||||
根据您使用的终端类型配置环境变量:
|
||||
|
||||
##### PowerShell(推荐)
|
||||
|
||||
```powershell
|
||||
# 必需:设置认证 Token(API Key 格式)
|
||||
$env:UNI_LAB_AUTH_TOKEN = "Api xxxx"
|
||||
|
||||
# 可选:自定义服务器地址(默认为 test 环境)
|
||||
$env:UNI_LAB_BASE_URL = "https://uni-lab.test.bohrium.com"
|
||||
|
||||
# 可选:自定义上传场景(默认为 job)
|
||||
$env:UNI_LAB_UPLOAD_SCENE = "job"
|
||||
|
||||
# 验证是否设置成功
|
||||
echo $env:UNI_LAB_AUTH_TOKEN
|
||||
```
|
||||
|
||||
##### CMD / 命令提示符
|
||||
|
||||
```cmd
|
||||
REM 必需:设置认证 Token(API Key 格式)
|
||||
set UNI_LAB_AUTH_TOKEN=Api xxxx
|
||||
|
||||
REM 可选:自定义配置
|
||||
set UNI_LAB_BASE_URL=https://uni-lab.test.bohrium.com
|
||||
set UNI_LAB_UPLOAD_SCENE=job
|
||||
|
||||
REM 验证是否设置成功
|
||||
echo %UNI_LAB_AUTH_TOKEN%
|
||||
```
|
||||
|
||||
##### Linux/Mac
|
||||
|
||||
```bash
|
||||
# 必需:设置认证 Token(API Key 格式)
|
||||
export UNI_LAB_AUTH_TOKEN="Api xxxx"
|
||||
|
||||
# 可选:自定义配置
|
||||
export UNI_LAB_BASE_URL="https://uni-lab.test.bohrium.com"
|
||||
export UNI_LAB_UPLOAD_SCENE="job"
|
||||
|
||||
# 验证是否设置成功
|
||||
echo $UNI_LAB_AUTH_TOKEN
|
||||
```
|
||||
|
||||
#### 3. 获取认证 Token
|
||||
|
||||
> **重要**:从 Uni-Lab 主页 → 账号安全 中获取 API Key。
|
||||
|
||||
**获取步骤**:
|
||||
1. 登录 Uni-Lab 系统
|
||||
2. 进入主页 → 账号安全
|
||||
3. 复制 API Key
|
||||
|
||||
Token 格式示例:
|
||||
```
|
||||
Api 48ccxx336fba44f39e1e37db93xxxxx
|
||||
```
|
||||
|
||||
> **提示**:
|
||||
> - 如果 Token 已经包含 `Api ` 前缀,直接使用
|
||||
> - 如果没有前缀,代码会自动添加 `Api ` 前缀
|
||||
> - 旧版 `Bearer` JWT Token 格式仍然兼容
|
||||
|
||||
#### 4. 持久化配置(可选)
|
||||
|
||||
**临时配置**:上述命令设置的环境变量只在当前终端会话中有效。
|
||||
|
||||
**持久化方式 1:PowerShell 配置文件**
|
||||
```powershell
|
||||
# 编辑 PowerShell 配置文件
|
||||
notepad $PROFILE
|
||||
|
||||
# 在打开的文件中添加:
|
||||
$env:UNI_LAB_AUTH_TOKEN = "Api 你的API_Key"
|
||||
```
|
||||
|
||||
**持久化方式 2:Windows 系统环境变量**
|
||||
- 右键"此电脑" → "属性" → "高级系统设置" → "环境变量"
|
||||
- 添加用户变量或系统变量:
|
||||
- 变量名:`UNI_LAB_AUTH_TOKEN`
|
||||
- 变量值:`Api 你的API_Key`
|
||||
|
||||
### 使用流程
|
||||
|
||||
#### 步骤 1:启用 OSS 上传功能
|
||||
|
||||
**推荐方式:在 `device.json` 中配置**
|
||||
|
||||
编辑设备配置文件 `unilabos/devices/neware_battery_test_system/device.json`,在 `config` 中添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "NEWARE_BATTERY_TEST_SYSTEM",
|
||||
"config": {
|
||||
"ip": "127.0.0.1",
|
||||
"port": 502,
|
||||
"machine_id": 1,
|
||||
"oss_upload_enabled": true,
|
||||
"oss_prefix": "neware_backup/2025-12"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
- `oss_upload_enabled`: 设置为 `true` 启用 OSS 上传
|
||||
- `oss_prefix`: OSS 文件路径前缀,建议按日期或项目组织(暂时未使用,保留接口兼容性)
|
||||
|
||||
**其他方式:通过初始化参数**
|
||||
|
||||
```python
|
||||
device = NewareBatteryTestSystem(
|
||||
ip="127.0.0.1",
|
||||
port=502,
|
||||
oss_upload_enabled=True, # 启用 OSS 上传
|
||||
oss_prefix="neware_backup/2025-12" # 可选:自定义路径前缀
|
||||
)
|
||||
```
|
||||
|
||||
**配置完成后,重启 ROS 节点使配置生效。**
|
||||
|
||||
#### 步骤 2:提交测试任务
|
||||
|
||||
使用 `submit_from_csv` 提交测试任务:
|
||||
|
||||
```python
|
||||
result = device.submit_from_csv(
|
||||
csv_path="test_data.csv",
|
||||
output_dir="D:/neware_output"
|
||||
)
|
||||
```
|
||||
|
||||
此时会创建以下目录结构:
|
||||
```
|
||||
D:/neware_output/
|
||||
├── xml_dir/ # XML 配置文件
|
||||
└── backup_dir/ # 测试数据备份(由新威设备生成)
|
||||
```
|
||||
|
||||
#### 步骤 3:等待测试完成
|
||||
|
||||
等待新威设备完成测试,备份文件会生成到 `backup_dir` 中。
|
||||
|
||||
#### 步骤 4:上传备份文件到 OSS
|
||||
|
||||
**方法 A:使用默认设置(推荐)**
|
||||
```python
|
||||
# 自动使用最近一次的 backup_dir,上传所有文件
|
||||
result = device.upload_backup_to_oss()
|
||||
```
|
||||
|
||||
**方法 B:指定备份目录**
|
||||
```python
|
||||
# 手动指定备份目录
|
||||
result = device.upload_backup_to_oss(
|
||||
backup_dir="D:/neware_output/backup_dir"
|
||||
)
|
||||
```
|
||||
|
||||
**方法 C:筛选特定文件**
|
||||
```python
|
||||
# 仅上传 CSV 文件
|
||||
result = device.upload_backup_to_oss(
|
||||
backup_dir="D:/neware_output/backup_dir",
|
||||
file_pattern="*.csv"
|
||||
)
|
||||
|
||||
# 仅上传特定电池编号的文件
|
||||
result = device.upload_backup_to_oss(
|
||||
file_pattern="Battery_A001_*.nda"
|
||||
)
|
||||
```
|
||||
|
||||
### 返回结果示例
|
||||
|
||||
**成功上传所有文件**:
|
||||
```python
|
||||
{
|
||||
"return_info": "全部上传成功: 15/15 个文件",
|
||||
"success": True,
|
||||
"uploaded_count": 15,
|
||||
"total_count": 15,
|
||||
"failed_files": [],
|
||||
"uploaded_files": [
|
||||
{
|
||||
"filename": "Battery_A001.ndax",
|
||||
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A001.ndax"
|
||||
},
|
||||
{
|
||||
"filename": "Battery_A002.ndax",
|
||||
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A002.ndax"
|
||||
}
|
||||
# ... 其他 13 个文件
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**部分上传成功**:
|
||||
```python
|
||||
{
|
||||
"return_info": "部分上传成功: 12/15 个文件,失败 3 个",
|
||||
"success": True,
|
||||
"uploaded_count": 12,
|
||||
"total_count": 15,
|
||||
"failed_files": ["Battery_A003.csv", "Battery_A007.csv", "test.log"],
|
||||
"uploaded_files": [
|
||||
{
|
||||
"filename": "Battery_A001.ndax",
|
||||
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A001.ndax"
|
||||
},
|
||||
{
|
||||
"filename": "Battery_A002.ndax",
|
||||
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A002.ndax"
|
||||
}
|
||||
# ... 其他 10 个成功上传的文件
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **说明**:`uploaded_files` 字段包含所有成功上传文件的详细信息:
|
||||
> - `filename`: 文件名(不含路径)
|
||||
> - `url`: 文件在 OSS 上的完整访问 URL
|
||||
|
||||
## 错误处理
|
||||
|
||||
### OSS 上传未启用
|
||||
|
||||
如果 `oss_upload_enabled=False`,调用 `upload_backup_to_oss` 会返回:
|
||||
```python
|
||||
{
|
||||
"return_info": "OSS 上传未启用 (oss_upload_enabled=False),跳过上传。备份目录: ...",
|
||||
"success": False,
|
||||
"uploaded_count": 0,
|
||||
"total_count": 0,
|
||||
"failed_files": []
|
||||
}
|
||||
```
|
||||
|
||||
**解决方法**:设置 `device.oss_upload_enabled = True`
|
||||
|
||||
### 环境变量未配置
|
||||
|
||||
如果缺少 `UNI_LAB_AUTH_TOKEN`,会返回:
|
||||
```python
|
||||
{
|
||||
"return_info": "OSS 环境变量配置错误: 请设置环境变量: UNI_LAB_AUTH_TOKEN",
|
||||
"success": False,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**解决方法**:按照前置条件配置环境变量
|
||||
|
||||
### 备份目录不存在
|
||||
|
||||
如果指定的备份目录不存在,会返回:
|
||||
```python
|
||||
{
|
||||
"return_info": "备份目录不存在: D:/neware_output/backup_dir",
|
||||
"success": False,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**解决方法**:检查目录路径是否正确,或等待测试生成备份文件
|
||||
|
||||
### API 认证失败
|
||||
|
||||
如果 Token 无效或过期,会返回:
|
||||
```python
|
||||
{
|
||||
"return_info": "获取凭证失败: 认证失败",
|
||||
"success": False,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**解决方法**:检查 Token 是否正确,或联系开发团队获取新 Token
|
||||
|
||||
## 技术细节
|
||||
|
||||
### OSS 上传流程(新方式)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[开始上传] --> B[验证配置和环境变量]
|
||||
B --> C[扫描备份目录]
|
||||
C --> D[筛选符合 pattern 的文件]
|
||||
D --> E[遍历每个文件]
|
||||
E --> F[调用 API 获取预签名 URL]
|
||||
F --> G{获取成功?}
|
||||
G -->|是| H[使用预签名 URL 上传文件]
|
||||
G -->|否| I[记录失败]
|
||||
H --> J{上传成功?}
|
||||
J -->|是| K[记录成功 + 文件 URL]
|
||||
J -->|否| I
|
||||
I --> L{还有文件?}
|
||||
K --> L
|
||||
L -->|是| E
|
||||
L -->|否| M[返回统计结果]
|
||||
```
|
||||
|
||||
### 上传 API 流程
|
||||
|
||||
1. **获取预签名 URL**
|
||||
- 请求:`GET /api/v1/applications/token?scene={scene}&filename={filename}`
|
||||
- 认证:`Authorization: Bearer {token}`
|
||||
- 响应:`{code: 0, data: {url: "预签名URL", path: "文件路径"}}`
|
||||
|
||||
2. **上传文件**
|
||||
- 请求:`PUT {预签名URL}`
|
||||
- 内容:文件二进制数据
|
||||
- 响应:HTTP 200 表示成功
|
||||
|
||||
3. **生成访问 URL**
|
||||
- 格式:`https://{OSS_PUBLIC_HOST}/{path}`
|
||||
- 示例:`https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/20251217/battery_data.csv`
|
||||
|
||||
### 日志记录
|
||||
|
||||
所有上传操作都会通过 ROS 日志系统记录:
|
||||
- `INFO` 级别:上传进度和成功信息
|
||||
- `WARNING` 级别:空目录、未启用等警告
|
||||
- `ERROR` 级别:上传失败、配置错误
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **上传时机**:`backup_dir` 中的文件是在新威设备执行测试过程中实时生成的,请确保测试已完成再上传。
|
||||
|
||||
2. **文件命名**:上传到 OSS 的文件会保留原始文件名,路径由统一 API 分配。
|
||||
|
||||
3. **网络要求**:上传需要稳定的网络连接到阿里云 OSS 服务。
|
||||
|
||||
4. **Token 有效期**:JWT Token 有过期时间,过期后需要重新获取。
|
||||
|
||||
5. **成本考虑**:OSS 存储和流量会产生费用,请根据需要合理设置文件筛选规则。
|
||||
|
||||
6. **并发上传**:当前实现为串行上传,大量文件上传可能需要较长时间。
|
||||
|
||||
7. **文件大小限制**:请注意单个文件大小是否有上传限制(由统一 API 控制)。
|
||||
|
||||
## 兼容性
|
||||
|
||||
- ✅ **向后兼容**:默认 `oss_upload_enabled=False`,不影响现有系统
|
||||
- ✅ **可选功能**:仅在需要时启用
|
||||
- ✅ **独立操作**:上传失败不会影响测试任务的提交和执行
|
||||
- ⚠️ **环境变量变更**:需要更新环境变量配置(从 OSS AK/SK 改为 JWT Token)
|
||||
|
||||
## 迁移指南
|
||||
|
||||
如果您之前使用 `oss2` 库方式,请按以下步骤迁移:
|
||||
|
||||
### 1. 卸载旧依赖(可选)
|
||||
```bash
|
||||
pip uninstall oss2
|
||||
```
|
||||
|
||||
### 2. 删除旧环境变量
|
||||
```powershell
|
||||
# PowerShell
|
||||
Remove-Item Env:\OSS_ACCESS_KEY_ID
|
||||
Remove-Item Env:\OSS_ACCESS_KEY_SECRET
|
||||
Remove-Item Env:\OSS_BUCKET_NAME
|
||||
Remove-Item Env:\OSS_ENDPOINT
|
||||
```
|
||||
|
||||
### 3. 设置新环境变量
|
||||
```powershell
|
||||
# PowerShell
|
||||
$env:UNI_LAB_AUTH_TOKEN = "Bearer 你的token..."
|
||||
```
|
||||
|
||||
### 4. 测试上传功能
|
||||
```python
|
||||
# 验证上传是否正常工作
|
||||
result = device.upload_backup_to_oss(backup_dir="测试目录")
|
||||
print(result)
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 为什么要从 `oss2` 改为统一 API?**
|
||||
A: 为了与团队其他系统保持一致,简化配置,并统一认证方式。
|
||||
|
||||
**Q: Token 在哪里获取?**
|
||||
A: 请联系开发团队获取有效的 JWT Token。
|
||||
|
||||
**Q: Token 过期了怎么办?**
|
||||
A: 重新获取新的 Token 并更新环境变量 `UNI_LAB_AUTH_TOKEN`。
|
||||
|
||||
**Q: 可以自定义上传路径吗?**
|
||||
A: 当前版本路径由统一 API 自动分配,`oss_prefix` 参数暂不使用(保留接口兼容性)。
|
||||
|
||||
**Q: 为什么不在 `submit_from_csv` 中自动上传?**
|
||||
A: 因为备份文件在测试进行中逐步生成,方法返回时可能文件尚未完全生成,因此提供独立的上传方法更灵活。
|
||||
|
||||
**Q: 上传后如何访问文件?**
|
||||
A: 上传成功后会返回文件访问 URL,格式为 `https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/{path}`
|
||||
|
||||
**Q: 如何删除已上传的文件?**
|
||||
A: 需要通过 OSS 控制台或 API 操作,本功能仅负责上传。
|
||||
|
||||
## 验证上传结果
|
||||
|
||||
### 方法1:通过阿里云控制台查看
|
||||
|
||||
1. 登录 [阿里云 OSS 控制台](https://oss.console.aliyun.com/)
|
||||
2. 点击左侧 **Bucket列表**
|
||||
3. 选择 `uni-lab-test` Bucket
|
||||
4. 点击 **文件管理**
|
||||
5. 查看上传的文件列表
|
||||
|
||||
### 方法2:使用返回的文件 URL
|
||||
|
||||
上传成功后,`upload_file_to_oss()` 会返回文件访问 URL:
|
||||
```python
|
||||
url = upload_file_to_oss("local_file.csv")
|
||||
print(f"文件访问 URL: {url}")
|
||||
# 输出示例:https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/20251217/local_file.csv
|
||||
```
|
||||
|
||||
> **注意**:OSS 文件默认为私有访问,直接访问 URL 可能需要签名认证。
|
||||
|
||||
### 方法3:使用 ossutil 命令行工具
|
||||
|
||||
安装 [ossutil](https://help.aliyun.com/document_detail/120075.html) 后:
|
||||
|
||||
```bash
|
||||
# 列出文件
|
||||
ossutil ls oss://uni-lab-test/job/
|
||||
|
||||
# 下载文件到本地
|
||||
ossutil cp oss://uni-lab-test/job/20251217/文件名 ./本地路径
|
||||
|
||||
# 生成签名URL(有效期1小时)
|
||||
ossutil sign oss://uni-lab-test/job/20251217/文件名 --timeout 3600
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
- **2025-12-17**: v2.0(重大更新)
|
||||
- ⚠️ 从 `oss2` 库改为统一 API 方式
|
||||
- 简化环境变量配置(仅需 JWT Token)
|
||||
- 新增 `get_upload_token()` 和 `upload_file_with_presigned_url()` 函数
|
||||
- `upload_file_to_oss()` 返回值改为文件访问 URL
|
||||
- 更新文档和迁移指南
|
||||
|
||||
- **2025-12-15**: v1.1
|
||||
- 添加初始化参数 `oss_upload_enabled` 和 `oss_prefix`
|
||||
- 支持在 `device.json` 中配置 OSS 上传
|
||||
- 更新使用说明,添加验证方法
|
||||
|
||||
- **2025-12-13**: v1.0 初始版本
|
||||
- 添加 OSS 上传工具函数(基于 `oss2` 库)
|
||||
- 创建 `upload_backup_to_oss` 动作方法
|
||||
- 支持文件筛选和自定义 OSS 路径
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Uni-Lab 统一文件上传 API 文档](https://uni-lab.test.bohrium.com/api/docs)(如有)
|
||||
- [阿里云 OSS 控制台](https://oss.console.aliyun.com/)
|
||||
- [ossutil 工具文档](https://help.aliyun.com/document_detail/120075.html)
|
||||
@@ -0,0 +1,574 @@
|
||||
# Neware Battery Test System - OSS Upload Feature
|
||||
|
||||
## Overview
|
||||
|
||||
This update adds **Aliyun OSS file upload functionality** to the Neware Battery Test System using a unified API approach, allowing test data backup files to be uploaded to cloud storage.
|
||||
|
||||
## Version Updates
|
||||
|
||||
### ⚠️ Breaking Changes (2025-12-17)
|
||||
|
||||
This update changes the OSS upload method from **`oss2` library** to **unified API approach** to align with other team systems.
|
||||
|
||||
**Main Changes**:
|
||||
- ✅ Use `requests` library
|
||||
- ✅ Upload via presigned URLs obtained through unified API
|
||||
- ✅ Simplified environment variable configuration (only API Key required)
|
||||
- ✅ Returns file access URLs
|
||||
|
||||
## Main Changes
|
||||
|
||||
### 1. OSS Upload Functions Refactored (Lines 30-200)
|
||||
|
||||
#### New Functions
|
||||
|
||||
- **`get_upload_token(base_url, auth_token, scene, filename)`**
|
||||
Obtain presigned URL for file upload from unified API
|
||||
|
||||
- **`upload_file_with_presigned_url(upload_info, file_path)`**
|
||||
Upload file to OSS using presigned URL
|
||||
|
||||
#### Updated Functions
|
||||
|
||||
- **`upload_file_to_oss(local_file_path, oss_object_name)`**
|
||||
Upload single file to Aliyun OSS (using unified API approach)
|
||||
- Return value changed: returns file access URL on success, `False` on failure
|
||||
|
||||
- **`upload_files_to_oss(file_paths, oss_prefix)`**
|
||||
Batch upload file list
|
||||
- `oss_prefix` parameter retained but not used (interface compatibility)
|
||||
|
||||
- **`upload_directory_to_oss(local_dir, oss_prefix)`**
|
||||
Upload entire directory
|
||||
- Simplified implementation, uploads using filenames directly
|
||||
|
||||
### 2. Simplified Environment Variable Configuration
|
||||
|
||||
#### Old Method (Deprecated)
|
||||
```bash
|
||||
# ❌ No longer used
|
||||
OSS_ACCESS_KEY_ID
|
||||
OSS_ACCESS_KEY_SECRET
|
||||
OSS_BUCKET_NAME
|
||||
OSS_ENDPOINT
|
||||
```
|
||||
|
||||
#### New Method (Recommended)
|
||||
```bash
|
||||
# ✅ Required
|
||||
UNI_LAB_AUTH_TOKEN # API Key format: "Api xxxxxx"
|
||||
|
||||
# ✅ Optional (with defaults)
|
||||
UNI_LAB_BASE_URL (default: https://uni-lab.test.bohrium.com)
|
||||
UNI_LAB_UPLOAD_SCENE (default: job, other values will be changed to default)
|
||||
```
|
||||
|
||||
### 3. Initialization Method (Unchanged)
|
||||
|
||||
OSS-related configuration parameters in `__init__` method:
|
||||
|
||||
```python
|
||||
# OSS upload configuration
|
||||
self.oss_upload_enabled = False # OSS upload disabled by default
|
||||
self.oss_prefix = "neware_backup" # OSS object path prefix
|
||||
self._last_backup_dir = None # Record last backup_dir
|
||||
```
|
||||
|
||||
**Default Behavior**: OSS upload is disabled by default (`oss_upload_enabled=False`), does not affect existing systems.
|
||||
|
||||
### 4. upload_backup_to_oss Method (Unchanged)
|
||||
|
||||
```python
|
||||
def upload_backup_to_oss(
|
||||
self,
|
||||
backup_dir: str = None,
|
||||
file_pattern: str = "*",
|
||||
oss_prefix: str = None
|
||||
) -> dict
|
||||
```
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
#### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
# requests library (usually pre-installed)
|
||||
pip install requests
|
||||
```
|
||||
|
||||
> **Note**: No longer need to install `oss2` library
|
||||
|
||||
#### 2. Configure Environment Variables
|
||||
|
||||
Configure environment variables based on your terminal type:
|
||||
|
||||
##### PowerShell (Recommended)
|
||||
|
||||
```powershell
|
||||
# Required: Set authentication Token (API Key format)
|
||||
$env:UNI_LAB_AUTH_TOKEN = "Api xxxx"
|
||||
|
||||
# Optional: Custom server URL (defaults to test environment)
|
||||
$env:UNI_LAB_BASE_URL = "https://uni-lab.test.bohrium.com"
|
||||
|
||||
# Optional: Custom upload scene (defaults to job)
|
||||
$env:UNI_LAB_UPLOAD_SCENE = "job"
|
||||
|
||||
# Verify if set successfully
|
||||
echo $env:UNI_LAB_AUTH_TOKEN
|
||||
```
|
||||
|
||||
##### CMD / Command Prompt
|
||||
|
||||
```cmd
|
||||
REM Required: Set authentication Token (API Key format)
|
||||
set UNI_LAB_AUTH_TOKEN=Api xxxx
|
||||
|
||||
REM Optional: Custom configuration
|
||||
set UNI_LAB_BASE_URL=https://uni-lab.test.bohrium.com
|
||||
set UNI_LAB_UPLOAD_SCENE=job
|
||||
|
||||
REM Verify if set successfully
|
||||
echo %UNI_LAB_AUTH_TOKEN%
|
||||
```
|
||||
|
||||
##### Linux/Mac
|
||||
|
||||
```bash
|
||||
# Required: Set authentication Token (API Key format)
|
||||
export UNI_LAB_AUTH_TOKEN="Api xxxx"
|
||||
|
||||
# Optional: Custom configuration
|
||||
export UNI_LAB_BASE_URL="https://uni-lab.test.bohrium.com"
|
||||
export UNI_LAB_UPLOAD_SCENE="job"
|
||||
|
||||
# Verify if set successfully
|
||||
echo $UNI_LAB_AUTH_TOKEN
|
||||
```
|
||||
|
||||
#### 3. Obtain Authentication Token
|
||||
|
||||
> **Important**: Obtain API Key from Uni-Lab Homepage → Account Security.
|
||||
|
||||
**Steps to Obtain**:
|
||||
1. Login to Uni-Lab system
|
||||
2. Go to Homepage → Account Security
|
||||
3. Copy your API Key
|
||||
|
||||
Token format example:
|
||||
```
|
||||
Api 48ccxx336fba44f39e1e37db93xxxxx
|
||||
```
|
||||
|
||||
> **Tips**:
|
||||
> - If Token already includes `Api ` prefix, use directly
|
||||
> - If no prefix, code will automatically add `Api ` prefix
|
||||
> - Old `Bearer` JWT Token format is still compatible
|
||||
|
||||
#### 4. Persistent Configuration (Optional)
|
||||
|
||||
**Temporary Configuration**: Environment variables set with the above commands are only valid for the current terminal session.
|
||||
|
||||
**Persistence Method 1: PowerShell Profile**
|
||||
```powershell
|
||||
# Edit PowerShell profile
|
||||
notepad $PROFILE
|
||||
|
||||
# Add to the opened file:
|
||||
$env:UNI_LAB_AUTH_TOKEN = "Api your_API_Key"
|
||||
```
|
||||
|
||||
**Persistence Method 2: Windows System Environment Variables**
|
||||
- Right-click "This PC" → "Properties" → "Advanced system settings" → "Environment Variables"
|
||||
- Add user or system variable:
|
||||
- Variable name: `UNI_LAB_AUTH_TOKEN`
|
||||
- Variable value: `Api your_API_Key`
|
||||
|
||||
### Usage Workflow
|
||||
|
||||
#### Step 1: Enable OSS Upload Feature
|
||||
|
||||
**Recommended: Configure in `device.json`**
|
||||
|
||||
Edit device configuration file `unilabos/devices/neware_battery_test_system/device.json`, add to `config`:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "NEWARE_BATTERY_TEST_SYSTEM",
|
||||
"config": {
|
||||
"ip": "127.0.0.1",
|
||||
"port": 502,
|
||||
"machine_id": 1,
|
||||
"oss_upload_enabled": true,
|
||||
"oss_prefix": "neware_backup/2025-12"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Parameter Description**:
|
||||
- `oss_upload_enabled`: Set to `true` to enable OSS upload
|
||||
- `oss_prefix`: OSS file path prefix, recommended to organize by date or project (currently unused, retained for interface compatibility)
|
||||
|
||||
**Alternative: Via Initialization Parameters**
|
||||
|
||||
```python
|
||||
device = NewareBatteryTestSystem(
|
||||
ip="127.0.0.1",
|
||||
port=502,
|
||||
oss_upload_enabled=True, # Enable OSS upload
|
||||
oss_prefix="neware_backup/2025-12" # Optional: custom path prefix
|
||||
)
|
||||
```
|
||||
|
||||
**After configuration, restart the ROS node for changes to take effect.**
|
||||
|
||||
#### Step 2: Submit Test Tasks
|
||||
|
||||
Use `submit_from_csv` to submit test tasks:
|
||||
|
||||
```python
|
||||
result = device.submit_from_csv(
|
||||
csv_path="test_data.csv",
|
||||
output_dir="D:/neware_output"
|
||||
)
|
||||
```
|
||||
|
||||
This creates the following directory structure:
|
||||
```
|
||||
D:/neware_output/
|
||||
├── xml_dir/ # XML configuration files
|
||||
└── backup_dir/ # Test data backup (generated by Neware device)
|
||||
```
|
||||
|
||||
#### Step 3: Wait for Test Completion
|
||||
|
||||
Wait for the Neware device to complete testing. Backup files will be generated in the `backup_dir`.
|
||||
|
||||
#### Step 4: Upload Backup Files to OSS
|
||||
|
||||
**Method A: Use Default Settings (Recommended)**
|
||||
```python
|
||||
# Automatically uses the last backup_dir, uploads all files
|
||||
result = device.upload_backup_to_oss()
|
||||
```
|
||||
|
||||
**Method B: Specify Backup Directory**
|
||||
```python
|
||||
# Manually specify backup directory
|
||||
result = device.upload_backup_to_oss(
|
||||
backup_dir="D:/neware_output/backup_dir"
|
||||
)
|
||||
```
|
||||
|
||||
**Method C: Filter Specific Files**
|
||||
```python
|
||||
# Upload only CSV files
|
||||
result = device.upload_backup_to_oss(
|
||||
backup_dir="D:/neware_output/backup_dir",
|
||||
file_pattern="*.csv"
|
||||
)
|
||||
|
||||
# Upload files for specific battery IDs
|
||||
result = device.upload_backup_to_oss(
|
||||
file_pattern="Battery_A001_*.nda"
|
||||
)
|
||||
```
|
||||
|
||||
### Return Result Examples
|
||||
|
||||
**All Files Uploaded Successfully**:
|
||||
```python
|
||||
{
|
||||
"return_info": "All uploads successful: 15/15 files",
|
||||
"success": True,
|
||||
"uploaded_count": 15,
|
||||
"total_count": 15,
|
||||
"failed_files": [],
|
||||
"uploaded_files": [
|
||||
{
|
||||
"filename": "Battery_A001.ndax",
|
||||
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A001.ndax"
|
||||
},
|
||||
{
|
||||
"filename": "Battery_A002.ndax",
|
||||
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A002.ndax"
|
||||
}
|
||||
# ... other 13 files
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Partial Upload Success**:
|
||||
```python
|
||||
{
|
||||
"return_info": "Partial upload success: 12/15 files, 3 failed",
|
||||
"success": True,
|
||||
"uploaded_count": 12,
|
||||
"total_count": 15,
|
||||
"failed_files": ["Battery_A003.csv", "Battery_A007.csv", "test.log"],
|
||||
"uploaded_files": [
|
||||
{
|
||||
"filename": "Battery_A001.ndax",
|
||||
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A001.ndax"
|
||||
},
|
||||
{
|
||||
"filename": "Battery_A002.ndax",
|
||||
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A002.ndax"
|
||||
}
|
||||
# ... other 10 successfully uploaded files
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: The `uploaded_files` field contains detailed information for all successfully uploaded files:
|
||||
> - `filename`: Filename (without path)
|
||||
> - `url`: Complete OSS access URL for the file
|
||||
|
||||
## Error Handling
|
||||
|
||||
### OSS Upload Not Enabled
|
||||
|
||||
If `oss_upload_enabled=False`, calling `upload_backup_to_oss` returns:
|
||||
```python
|
||||
{
|
||||
"return_info": "OSS upload not enabled (oss_upload_enabled=False), skipping upload. Backup directory: ...",
|
||||
"success": False,
|
||||
"uploaded_count": 0,
|
||||
"total_count": 0,
|
||||
"failed_files": []
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Set `device.oss_upload_enabled = True`
|
||||
|
||||
### Environment Variables Not Configured
|
||||
|
||||
If `UNI_LAB_AUTH_TOKEN` is missing, returns:
|
||||
```python
|
||||
{
|
||||
"return_info": "OSS environment variable configuration error: Please set environment variable: UNI_LAB_AUTH_TOKEN",
|
||||
"success": False,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Configure environment variables as per prerequisites
|
||||
|
||||
### Backup Directory Does Not Exist
|
||||
|
||||
If specified backup directory doesn't exist, returns:
|
||||
```python
|
||||
{
|
||||
"return_info": "Backup directory does not exist: D:/neware_output/backup_dir",
|
||||
"success": False,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Check if directory path is correct, or wait for test to generate backup files
|
||||
|
||||
### API Authentication Failed
|
||||
|
||||
If Token is invalid or expired, returns:
|
||||
```python
|
||||
{
|
||||
"return_info": "Failed to get credentials: Authentication failed",
|
||||
"success": False,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Check if Token is correct, or contact development team for new Token
|
||||
|
||||
## Technical Details
|
||||
|
||||
### OSS Upload Process (New Method)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Start Upload] --> B[Verify Configuration and Environment Variables]
|
||||
B --> C[Scan Backup Directory]
|
||||
C --> D[Filter Files Matching Pattern]
|
||||
D --> E[Iterate Each File]
|
||||
E --> F[Call API to Get Presigned URL]
|
||||
F --> G{Success?}
|
||||
G -->|Yes| H[Upload File Using Presigned URL]
|
||||
G -->|No| I[Record Failure]
|
||||
H --> J{Upload Success?}
|
||||
J -->|Yes| K[Record Success + File URL]
|
||||
J -->|No| I
|
||||
I --> L{More Files?}
|
||||
K --> L
|
||||
L -->|Yes| E
|
||||
L -->|No| M[Return Statistics]
|
||||
```
|
||||
|
||||
### Upload API Flow
|
||||
|
||||
1. **Get Presigned URL**
|
||||
- Request: `GET /api/v1/lab/storage/token?scene={scene}&filename={filename}&path={path}`
|
||||
- Authentication: `Authorization: Api {api_key}` or `Authorization: Bearer {token}`
|
||||
- Response: `{code: 0, data: {url: "presigned_url", path: "file_path"}}`
|
||||
|
||||
2. **Upload File**
|
||||
- Request: `PUT {presigned_url}`
|
||||
- Content: File binary data
|
||||
- Response: HTTP 200 indicates success
|
||||
|
||||
3. **Generate Access URL**
|
||||
- Format: `https://{OSS_PUBLIC_HOST}/{path}`
|
||||
- Example: `https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/20251217/battery_data.csv`
|
||||
|
||||
### Logging
|
||||
|
||||
All upload operations are logged through ROS logging system:
|
||||
- `INFO` level: Upload progress and success information
|
||||
- `WARNING` level: Empty directory, not enabled warnings
|
||||
- `ERROR` level: Upload failures, configuration errors
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Upload Timing**: Files in `backup_dir` are generated in real-time during test execution. Ensure testing is complete before uploading.
|
||||
|
||||
2. **File Naming**: Files uploaded to OSS retain original filenames. Paths are assigned by unified API.
|
||||
|
||||
3. **Network Requirements**: Upload requires stable network connection to Aliyun OSS service.
|
||||
|
||||
4. **Token Expiration**: JWT Tokens have expiration time. Need to obtain new token after expiration.
|
||||
|
||||
5. **Cost Considerations**: OSS storage and traffic incur costs. Set file filtering rules appropriately.
|
||||
|
||||
6. **Concurrent Upload**: Current implementation uses serial upload. Large number of files may take considerable time.
|
||||
|
||||
7. **File Size Limits**: Note single file size upload limits (controlled by unified API).
|
||||
|
||||
## Compatibility
|
||||
|
||||
- ✅ **Backward Compatible**: Default `oss_upload_enabled=False`, does not affect existing systems
|
||||
- ✅ **Optional Feature**: Enable only when needed
|
||||
- ✅ **Independent Operation**: Upload failures do not affect test task submission and execution
|
||||
- ⚠️ **Environment Variable Changes**: Need to update environment variable configuration (from OSS AK/SK to API Key)
|
||||
|
||||
## Migration Guide
|
||||
|
||||
If you previously used the `oss2` library method, follow these steps to migrate:
|
||||
|
||||
### 1. Uninstall Old Dependencies (Optional)
|
||||
```bash
|
||||
pip uninstall oss2
|
||||
```
|
||||
|
||||
### 2. Remove Old Environment Variables
|
||||
```powershell
|
||||
# PowerShell
|
||||
Remove-Item Env:\OSS_ACCESS_KEY_ID
|
||||
Remove-Item Env:\OSS_ACCESS_KEY_SECRET
|
||||
Remove-Item Env:\OSS_BUCKET_NAME
|
||||
Remove-Item Env:\OSS_ENDPOINT
|
||||
```
|
||||
|
||||
### 3. Set New Environment Variables
|
||||
```powershell
|
||||
# PowerShell
|
||||
$env:UNI_LAB_AUTH_TOKEN = "Api your_API_Key"
|
||||
```
|
||||
|
||||
### 4. Test Upload Functionality
|
||||
```python
|
||||
# Verify upload works correctly
|
||||
result = device.upload_backup_to_oss(backup_dir="test_directory")
|
||||
print(result)
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Why change from `oss2` to unified API?**
|
||||
A: To maintain consistency with other team systems, simplify configuration, and unify authentication methods.
|
||||
|
||||
**Q: Where to get the Token?**
|
||||
A: Obtain API Key from Uni-Lab Homepage → Account Security.
|
||||
|
||||
**Q: What if Token expires?**
|
||||
A: Obtain a new API Key and update the `UNI_LAB_AUTH_TOKEN` environment variable.
|
||||
|
||||
**Q: Can I customize upload paths?**
|
||||
A: Current version has paths automatically assigned by unified API. `oss_prefix` parameter is currently unused (retained for interface compatibility).
|
||||
|
||||
**Q: Why not auto-upload in `submit_from_csv`?**
|
||||
A: Because backup files are generated progressively during testing, they may not be fully generated when the method returns. A separate upload method provides more flexibility.
|
||||
|
||||
**Q: How to access files after upload?**
|
||||
A: Upload success returns file access URL in format `https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/{path}`
|
||||
|
||||
**Q: How to delete uploaded files?**
|
||||
A: Need to operate through OSS console or API. This feature only handles uploads.
|
||||
|
||||
## Verifying Upload Results
|
||||
|
||||
### Method 1: Via Aliyun Console
|
||||
|
||||
1. Login to [Aliyun OSS Console](https://oss.console.aliyun.com/)
|
||||
2. Click **Bucket List** on the left
|
||||
3. Select the `uni-lab-test` Bucket
|
||||
4. Click **File Management**
|
||||
5. View uploaded file list
|
||||
|
||||
### Method 2: Using Returned File URL
|
||||
|
||||
After successful upload, `upload_file_to_oss()` returns file access URL:
|
||||
```python
|
||||
url = upload_file_to_oss("local_file.csv")
|
||||
print(f"File access URL: {url}")
|
||||
# Example output: https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/20251217/local_file.csv
|
||||
```
|
||||
|
||||
> **Note**: OSS files are private by default, direct URL access may require signature authentication.
|
||||
|
||||
### Method 3: Using ossutil CLI Tool
|
||||
|
||||
After installing [ossutil](https://help.aliyun.com/document_detail/120075.html):
|
||||
|
||||
```bash
|
||||
# List files
|
||||
ossutil ls oss://uni-lab-test/job/
|
||||
|
||||
# Download file to local
|
||||
ossutil cp oss://uni-lab-test/job/20251217/filename ./local_path
|
||||
|
||||
# Generate signed URL (valid for 1 hour)
|
||||
ossutil sign oss://uni-lab-test/job/20251217/filename --timeout 3600
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
- **2025-12-17**: v2.0 (Major Update)
|
||||
- ⚠️ Changed from `oss2` library to unified API approach
|
||||
- Simplified environment variable configuration (only API Key required)
|
||||
- Added `get_upload_token()` and `upload_file_with_presigned_url()` functions
|
||||
- `upload_file_to_oss()` return value changed to file access URL
|
||||
- Updated documentation and migration guide
|
||||
- Token format: Support both `Api Key` and `Bearer JWT`
|
||||
- API endpoint: `/api/v1/lab/storage/token`
|
||||
- Scene parameter: Fixed to `job` (other values changed to `default`)
|
||||
|
||||
- **2025-12-15**: v1.1
|
||||
- Added initialization parameters `oss_upload_enabled` and `oss_prefix`
|
||||
- Support OSS upload configuration in `device.json`
|
||||
- Updated usage guide, added verification methods
|
||||
|
||||
- **2025-12-13**: v1.0 Initial Version
|
||||
- Added OSS upload utility functions (based on `oss2` library)
|
||||
- Created `upload_backup_to_oss` action method
|
||||
- Support file filtering and custom OSS paths
|
||||
|
||||
## References
|
||||
|
||||
- [Uni-Lab Unified File Upload API Documentation](https://uni-lab.test.bohrium.com/api/docs) (if available)
|
||||
- [Aliyun OSS Console](https://oss.console.aliyun.com/)
|
||||
- [ossutil Tool Documentation](https://help.aliyun.com/document_detail/120075.html)
|
||||
35
unilabos/devices/neware_battery_test_system/device.json
Normal file
35
unilabos/devices/neware_battery_test_system/device.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "NEWARE_BATTERY_TEST_SYSTEM",
|
||||
"name": "Neware Battery Test System",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "neware_battery_test_system",
|
||||
"position": {
|
||||
"x": 620.0,
|
||||
"y": 200.0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"ip": "127.0.0.1",
|
||||
"port": 502,
|
||||
"machine_id": 1,
|
||||
"devtype": "27",
|
||||
"timeout": 20,
|
||||
"size_x": 500.0,
|
||||
"size_y": 500.0,
|
||||
"size_z": 2000.0,
|
||||
"oss_upload_enabled": true,
|
||||
"oss_prefix": "neware_backup/2025-12"
|
||||
},
|
||||
"data": {
|
||||
"功能说明": "新威电池测试系统,提供720通道监控和CSV批量提交功能",
|
||||
"监控功能": "支持720个通道的实时状态监控、2盘电池物料管理、状态导出等",
|
||||
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务。CSV必须包含: Battery_Code, Pole_Weight, 集流体质量, 活性物质含量, 克容量mah/g, 电池体系, 设备号, 排号, 通道号"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,282 +1,649 @@
|
||||
import sys
|
||||
import threading
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import re
|
||||
import time
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Contains drivers for:
|
||||
1. SyringePump: Runze Fluid SY-03B (ASCII)
|
||||
2. EmmMotor: Emm V5.0 Closed-loop Stepper (Modbus-RTU variant)
|
||||
3. XKCSensor: XKC Non-contact Level Sensor (Modbus-RTU)
|
||||
"""
|
||||
|
||||
class ChinweDevice:
|
||||
import socket
|
||||
import serial
|
||||
import time
|
||||
import threading
|
||||
import struct
|
||||
import re
|
||||
import traceback
|
||||
import queue
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
try:
|
||||
from unilabos.device_comms.universal_driver import UniversalDriver
|
||||
except ImportError:
|
||||
import logging
|
||||
class UniversalDriver:
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
def execute_command_from_outer(self, command: str):
|
||||
pass
|
||||
|
||||
# ==============================================================================
|
||||
# 1. Transport Layer (通信层)
|
||||
# ==============================================================================
|
||||
|
||||
class TransportManager:
|
||||
"""
|
||||
ChinWe设备控制类
|
||||
提供串口通信、电机控制、传感器数据读取等功能
|
||||
统一通信管理类。
|
||||
自动识别 串口 (Serial) 或 网络 (TCP) 连接。
|
||||
"""
|
||||
|
||||
def __init__(self, port: str, baudrate: int = 115200, debug: bool = False):
|
||||
"""
|
||||
初始化ChinWe设备
|
||||
|
||||
Args:
|
||||
port: 串口名称,如果为None则自动检测
|
||||
baudrate: 波特率,默认115200
|
||||
"""
|
||||
self.debug = debug
|
||||
def __init__(self, port: str, baudrate: int = 9600, timeout: float = 3.0, logger=None):
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.serial_port: Optional[serial.Serial] = None
|
||||
self._voltage: float = 0.0
|
||||
self._ec_value: float = 0.0
|
||||
self._ec_adc_value: int = 0
|
||||
self.timeout = timeout
|
||||
self.logger = logger
|
||||
self.lock = threading.RLock() # 线程锁,确保多设备共用一个连接时不冲突
|
||||
|
||||
self.is_tcp = False
|
||||
self.serial = None
|
||||
self.socket = None
|
||||
|
||||
# 简单判断: 如果包含 ':' (如 192.168.1.1:8899) 或者看起来像 IP,则认为是 TCP
|
||||
if ':' in self.port or (self.port.count('.') == 3 and not self.port.startswith('/')):
|
||||
self.is_tcp = True
|
||||
self._connect_tcp()
|
||||
else:
|
||||
self._connect_serial()
|
||||
|
||||
def _log(self, msg):
|
||||
if self.logger:
|
||||
pass
|
||||
# self.logger.debug(f"[Transport] {msg}")
|
||||
|
||||
def _connect_tcp(self):
|
||||
try:
|
||||
if ':' in self.port:
|
||||
host, p = self.port.split(':')
|
||||
self.tcp_host = host
|
||||
self.tcp_port = int(p)
|
||||
else:
|
||||
self.tcp_host = self.port
|
||||
self.tcp_port = 8899 # 默认端口
|
||||
|
||||
# if self.logger: self.logger.info(f"Connecting TCP {self.tcp_host}:{self.tcp_port} ...")
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(self.timeout)
|
||||
self.socket.connect((self.tcp_host, self.tcp_port))
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"TCP connection failed: {e}")
|
||||
|
||||
def _connect_serial(self):
|
||||
try:
|
||||
# if self.logger: self.logger.info(f"Opening Serial {self.port} (Baud: {self.baudrate}) ...")
|
||||
self.serial = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
timeout=self.timeout
|
||||
)
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Serial open failed: {e}")
|
||||
|
||||
def close(self):
|
||||
"""关闭连接"""
|
||||
if self.is_tcp and self.socket:
|
||||
try: self.socket.close()
|
||||
except: pass
|
||||
elif not self.is_tcp and self.serial and self.serial.is_open:
|
||||
self.serial.close()
|
||||
|
||||
def clear_buffer(self):
|
||||
"""清空缓冲区 (Thread-safe)"""
|
||||
with self.lock:
|
||||
if self.is_tcp:
|
||||
self.socket.setblocking(False)
|
||||
try:
|
||||
while True:
|
||||
if not self.socket.recv(1024): break
|
||||
except: pass
|
||||
finally: self.socket.settimeout(self.timeout)
|
||||
else:
|
||||
self.serial.reset_input_buffer()
|
||||
|
||||
def write(self, data: bytes):
|
||||
"""发送原始字节"""
|
||||
with self.lock:
|
||||
if self.is_tcp:
|
||||
self.socket.sendall(data)
|
||||
else:
|
||||
self.serial.write(data)
|
||||
|
||||
def read(self, size: int) -> bytes:
|
||||
"""读取指定长度字节"""
|
||||
if self.is_tcp:
|
||||
data = b''
|
||||
start = time.time()
|
||||
while len(data) < size:
|
||||
if time.time() - start > self.timeout: break
|
||||
try:
|
||||
chunk = self.socket.recv(size - len(data))
|
||||
if not chunk: break
|
||||
data += chunk
|
||||
except socket.timeout: break
|
||||
return data
|
||||
else:
|
||||
return self.serial.read(size)
|
||||
|
||||
def send_ascii_command(self, command: str) -> str:
|
||||
"""
|
||||
发送 ASCII 字符串命令 (如注射泵指令),读取直到 '\r'。
|
||||
"""
|
||||
with self.lock:
|
||||
data = command.encode('ascii') if isinstance(command, str) else command
|
||||
self.clear_buffer()
|
||||
self.write(data)
|
||||
|
||||
# Read until \r
|
||||
if self.is_tcp:
|
||||
resp = b''
|
||||
start = time.time()
|
||||
while True:
|
||||
if time.time() - start > self.timeout: break
|
||||
try:
|
||||
char = self.socket.recv(1)
|
||||
if not char: break
|
||||
resp += char
|
||||
if char == b'\r': break
|
||||
except: break
|
||||
return resp.decode('ascii', errors='ignore').strip()
|
||||
else:
|
||||
return self.serial.read_until(b'\r').decode('ascii', errors='ignore').strip()
|
||||
|
||||
# ==============================================================================
|
||||
# 2. Syringe Pump Driver (注射泵)
|
||||
# ==============================================================================
|
||||
|
||||
class SyringePump:
|
||||
"""SY-03B 注射泵驱动 (ASCII协议)"""
|
||||
|
||||
CMD_INITIALIZE = "Z{speed},{drain_port},{output_port}R"
|
||||
CMD_SWITCH_VALVE = "I{port}R"
|
||||
CMD_ASPIRATE = "P{vol}R"
|
||||
CMD_DISPENSE = "D{vol}R"
|
||||
CMD_DISPENSE_ALL = "A0R"
|
||||
CMD_STOP = "TR"
|
||||
CMD_QUERY_STATUS = "Q"
|
||||
CMD_QUERY_PLUNGER = "?0"
|
||||
|
||||
def __init__(self, device_id: int, transport: TransportManager):
|
||||
if not 1 <= device_id <= 15:
|
||||
pass # Allow all IDs for now
|
||||
self.id = str(device_id)
|
||||
self.transport = transport
|
||||
|
||||
def _send(self, template: str, **kwargs) -> str:
|
||||
cmd = f"/{self.id}" + template.format(**kwargs) + "\r"
|
||||
return self.transport.send_ascii_command(cmd)
|
||||
|
||||
def is_busy(self) -> bool:
|
||||
"""查询繁忙状态"""
|
||||
resp = self._send(self.CMD_QUERY_STATUS)
|
||||
# 响应如 /0` (Ready, 0x60) 或 /0@ (Busy, 0x40)
|
||||
if len(resp) >= 3:
|
||||
status_byte = ord(resp[2])
|
||||
# Bit 5: 1=Ready, 0=Busy
|
||||
return (status_byte & 0x20) == 0
|
||||
return False
|
||||
|
||||
def wait_until_idle(self, timeout=30):
|
||||
"""阻塞等待直到空闲"""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
if not self.is_busy(): return
|
||||
time.sleep(0.5)
|
||||
# raise TimeoutError(f"Pump {self.id} wait idle timeout")
|
||||
pass
|
||||
|
||||
def initialize(self, drain_port=0, output_port=0, speed=10):
|
||||
"""初始化"""
|
||||
self._send(self.CMD_INITIALIZE, speed=speed, drain_port=drain_port, output_port=output_port)
|
||||
|
||||
def switch_valve(self, port: int):
|
||||
"""切换阀门 (1-8)"""
|
||||
self._send(self.CMD_SWITCH_VALVE, port=port)
|
||||
|
||||
def aspirate(self, steps: int):
|
||||
"""吸液 (相对步数)"""
|
||||
self._send(self.CMD_ASPIRATE, vol=steps)
|
||||
|
||||
def dispense(self, steps: int):
|
||||
"""排液 (相对步数)"""
|
||||
self._send(self.CMD_DISPENSE, vol=steps)
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
self._send(self.CMD_STOP)
|
||||
|
||||
def get_position(self) -> int:
|
||||
"""获取柱塞位置 (步数)"""
|
||||
resp = self._send(self.CMD_QUERY_PLUNGER)
|
||||
m = re.search(r'\d+', resp)
|
||||
return int(m.group()) if m else -1
|
||||
|
||||
# ==============================================================================
|
||||
# 3. Stepper Motor Driver (步进电机)
|
||||
# ==============================================================================
|
||||
|
||||
class EmmMotor:
|
||||
"""Emm V5.0 闭环步进电机驱动"""
|
||||
|
||||
def __init__(self, device_id: int, transport: TransportManager):
|
||||
self.id = device_id
|
||||
self.transport = transport
|
||||
|
||||
def _send(self, func_code: int, payload: list) -> bytes:
|
||||
with self.transport.lock:
|
||||
self.transport.clear_buffer()
|
||||
# 格式: [ID] [Func] [Data...] [Check=0x6B]
|
||||
body = [self.id, func_code] + payload
|
||||
body.append(0x6B) # Checksum
|
||||
self.transport.write(bytes(body))
|
||||
|
||||
# 根据指令不同,读取不同长度响应
|
||||
read_len = 10 if func_code in [0x31, 0x32, 0x35, 0x24, 0x27] else 4
|
||||
return self.transport.read(read_len)
|
||||
|
||||
def enable(self, on=True):
|
||||
"""使能 (True=锁轴, False=松轴)"""
|
||||
state = 1 if on else 0
|
||||
self._send(0xF3, [0xAB, state, 0])
|
||||
|
||||
def run_speed(self, speed_rpm: int, direction=0, acc=10):
|
||||
"""速度模式运行"""
|
||||
sp = struct.pack('>H', int(speed_rpm))
|
||||
self._send(0xF6, [direction, sp[0], sp[1], acc, 0])
|
||||
|
||||
def run_position(self, pulses: int, speed_rpm: int, direction=0, acc=10, absolute=False):
|
||||
"""位置模式运行"""
|
||||
sp = struct.pack('>H', int(speed_rpm))
|
||||
pl = struct.pack('>I', int(pulses))
|
||||
is_abs = 1 if absolute else 0
|
||||
self._send(0xFD, [direction, sp[0], sp[1], acc, pl[0], pl[1], pl[2], pl[3], is_abs, 0])
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
self._send(0xFE, [0x98, 0])
|
||||
|
||||
def set_zero(self):
|
||||
"""清零位置"""
|
||||
self._send(0x0A, [])
|
||||
|
||||
def get_position(self) -> int:
|
||||
"""获取当前脉冲位置"""
|
||||
resp = self._send(0x32, [])
|
||||
if len(resp) >= 8:
|
||||
sign = resp[2]
|
||||
val = struct.unpack('>I', resp[3:7])[0]
|
||||
return -val if sign == 1 else val
|
||||
return 0
|
||||
|
||||
# ==============================================================================
|
||||
# 4. Liquid Sensor Driver (液位传感器)
|
||||
# ==============================================================================
|
||||
|
||||
class XKCSensor:
|
||||
"""XKC RS485 液位传感器 (Modbus RTU)"""
|
||||
|
||||
def __init__(self, device_id: int, transport: TransportManager, threshold: int = 300):
|
||||
self.id = device_id
|
||||
self.transport = transport
|
||||
self.threshold = threshold
|
||||
|
||||
def _crc(self, data: bytes) -> bytes:
|
||||
crc = 0xFFFF
|
||||
for byte in data:
|
||||
crc ^= byte
|
||||
for _ in range(8):
|
||||
if crc & 0x0001: crc = (crc >> 1) ^ 0xA001
|
||||
else: crc >>= 1
|
||||
return struct.pack('<H', crc)
|
||||
|
||||
def read_level(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
读取液位。
|
||||
返回: {'level': bool, 'rssi': int}
|
||||
"""
|
||||
with self.transport.lock:
|
||||
self.transport.clear_buffer()
|
||||
# Modbus Read Registers: 01 03 00 01 00 02 CRC
|
||||
payload = struct.pack('>HH', 0x0001, 0x0002)
|
||||
msg = struct.pack('BB', self.id, 0x03) + payload
|
||||
msg += self._crc(msg)
|
||||
self.transport.write(msg)
|
||||
|
||||
# Read header
|
||||
h = self.transport.read(3) # Addr, Func, Len
|
||||
if len(h) < 3: return None
|
||||
length = h[2]
|
||||
|
||||
# Read body + CRC
|
||||
body = self.transport.read(length + 2)
|
||||
if len(body) < length + 2:
|
||||
# Firmware bug fix specific to some modules
|
||||
if len(body) == 4 and length == 4:
|
||||
pass
|
||||
else:
|
||||
return None
|
||||
|
||||
data = body[:-2]
|
||||
if len(data) == 2:
|
||||
rssi = data[1]
|
||||
elif len(data) >= 4:
|
||||
rssi = (data[2] << 8) | data[3]
|
||||
else:
|
||||
return None
|
||||
|
||||
return {
|
||||
'level': rssi > self.threshold,
|
||||
'rssi': rssi
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 5. Main Device Class (ChinweDevice)
|
||||
# ==============================================================================
|
||||
|
||||
class ChinweDevice(UniversalDriver):
|
||||
"""
|
||||
ChinWe 工作站主驱动
|
||||
继承自 UniversalDriver,管理所有子设备(泵、电机、传感器)
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = "192.168.1.200:8899", baudrate: int = 9600,
|
||||
pump_ids: List[int] = None, motor_ids: List[int] = None,
|
||||
sensor_id: int = 6, sensor_threshold: int = 300,
|
||||
timeout: float = 10.0):
|
||||
"""
|
||||
初始化 ChinWe 工作站
|
||||
:param port: 串口号 或 IP:Port
|
||||
:param baudrate: 串口波特率
|
||||
:param pump_ids: 注射泵 ID列表 (默认 [1, 2, 3])
|
||||
:param motor_ids: 步进电机 ID列表 (默认 [4, 5])
|
||||
:param sensor_id: 液位传感器 ID (默认 6)
|
||||
:param sensor_threshold: 传感器液位判定阈值
|
||||
:param timeout: 通信超时时间 (默认 10秒)
|
||||
"""
|
||||
super().__init__()
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.timeout = timeout
|
||||
self.mgr = None
|
||||
self._is_connected = False
|
||||
self.connect()
|
||||
|
||||
|
||||
# 默认配置
|
||||
if pump_ids is None: pump_ids = [1, 2, 3]
|
||||
if motor_ids is None: motor_ids = [4, 5]
|
||||
|
||||
# 配置信息
|
||||
self.pump_ids = pump_ids
|
||||
self.motor_ids = motor_ids
|
||||
self.sensor_id = sensor_id
|
||||
self.sensor_threshold = sensor_threshold
|
||||
|
||||
# 子设备实例容器
|
||||
self.pumps: Dict[int, SyringePump] = {}
|
||||
self.motors: Dict[int, EmmMotor] = {}
|
||||
self.sensor: Optional[XKCSensor] = None
|
||||
|
||||
# 轮询线程控制
|
||||
self._stop_event = threading.Event()
|
||||
self._poll_thread = None
|
||||
|
||||
# 实时状态缓存
|
||||
self.status_cache = {
|
||||
"sensor_rssi": 0,
|
||||
"sensor_level": False,
|
||||
"connected": False
|
||||
}
|
||||
|
||||
# 自动连接
|
||||
if self.port:
|
||||
self.connect()
|
||||
|
||||
def connect(self) -> bool:
|
||||
if self._is_connected: return True
|
||||
try:
|
||||
self.logger.info(f"Connecting to {self.port} (timeout={self.timeout})...")
|
||||
self.mgr = TransportManager(self.port, baudrate=self.baudrate, timeout=self.timeout, logger=self.logger)
|
||||
|
||||
# 初始化所有泵
|
||||
for pid in self.pump_ids:
|
||||
self.pumps[pid] = SyringePump(pid, self.mgr)
|
||||
|
||||
# 初始化所有电机
|
||||
for mid in self.motor_ids:
|
||||
self.motors[mid] = EmmMotor(mid, self.mgr)
|
||||
|
||||
# 初始化传感器
|
||||
self.sensor = XKCSensor(self.sensor_id, self.mgr, self.sensor_threshold)
|
||||
|
||||
self._is_connected = True
|
||||
self.status_cache["connected"] = True
|
||||
|
||||
# 启动轮询线程
|
||||
self._start_polling()
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Connection failed: {e}")
|
||||
self._is_connected = False
|
||||
self.status_cache["connected"] = False
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
self._stop_event.set()
|
||||
if self._poll_thread:
|
||||
self._poll_thread.join(timeout=2.0)
|
||||
|
||||
if self.mgr:
|
||||
self.mgr.close()
|
||||
|
||||
self._is_connected = False
|
||||
self.status_cache["connected"] = False
|
||||
self.logger.info("Disconnected.")
|
||||
|
||||
def _start_polling(self):
|
||||
"""启动传感器轮询线程"""
|
||||
if self._poll_thread and self._poll_thread.is_alive():
|
||||
return
|
||||
|
||||
self._stop_event.clear()
|
||||
self._poll_thread = threading.Thread(target=self._polling_loop, daemon=True, name="ChinwePoll")
|
||||
self._poll_thread.start()
|
||||
|
||||
def _polling_loop(self):
|
||||
"""轮询主循环"""
|
||||
self.logger.info("Sensor polling started.")
|
||||
error_count = 0
|
||||
while not self._stop_event.is_set():
|
||||
if not self._is_connected or not self.sensor:
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
try:
|
||||
# 获取传感器数据
|
||||
data = self.sensor.read_level()
|
||||
if data:
|
||||
self.status_cache["sensor_rssi"] = data['rssi']
|
||||
self.status_cache["sensor_level"] = data['level']
|
||||
error_count = 0
|
||||
else:
|
||||
error_count += 1
|
||||
|
||||
# 降低轮询频率防止总线拥塞
|
||||
time.sleep(0.2)
|
||||
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
if error_count > 10: # 连续错误记录日志
|
||||
# self.logger.error(f"Polling error: {e}")
|
||||
error_count = 0
|
||||
time.sleep(1)
|
||||
|
||||
# --- 对外暴露属性 (Properties) ---
|
||||
|
||||
@property
|
||||
def sensor_level(self) -> bool:
|
||||
return self.status_cache["sensor_level"]
|
||||
|
||||
@property
|
||||
def sensor_rssi(self) -> int:
|
||||
return self.status_cache["sensor_rssi"]
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""获取连接状态"""
|
||||
return self._is_connected and self.serial_port and self.serial_port.is_open
|
||||
|
||||
@property
|
||||
def voltage(self) -> float:
|
||||
"""获取电源电压值"""
|
||||
return self._voltage
|
||||
|
||||
@property
|
||||
def ec_value(self) -> float:
|
||||
"""获取电导率值 (ms/cm)"""
|
||||
return self._ec_value
|
||||
return self._is_connected
|
||||
|
||||
@property
|
||||
def ec_adc_value(self) -> int:
|
||||
"""获取EC ADC原始值"""
|
||||
return self._ec_adc_value
|
||||
|
||||
# --- 对外功能指令 (Actions) ---
|
||||
|
||||
@property
|
||||
def device_status(self) -> Dict[str, any]:
|
||||
"""
|
||||
获取设备状态信息
|
||||
|
||||
Returns:
|
||||
包含设备状态的字典
|
||||
"""
|
||||
return {
|
||||
"connected": self.is_connected,
|
||||
"port": self.port,
|
||||
"baudrate": self.baudrate,
|
||||
"voltage": self.voltage,
|
||||
"ec_value": self.ec_value,
|
||||
"ec_adc_value": self.ec_adc_value
|
||||
}
|
||||
|
||||
def connect(self, port: Optional[str] = None, baudrate: Optional[int] = None) -> bool:
|
||||
"""
|
||||
连接到串口设备
|
||||
|
||||
Args:
|
||||
port: 串口名称,如果为None则使用初始化时的port或自动检测
|
||||
baudrate: 波特率,如果为None则使用初始化时的baudrate
|
||||
|
||||
Returns:
|
||||
连接是否成功
|
||||
"""
|
||||
if self.is_connected:
|
||||
def pump_initialize(self, pump_id: int, drain_port=0, output_port=0, speed=10):
|
||||
"""指定泵初始化"""
|
||||
pump_id = int(pump_id)
|
||||
if pump_id in self.pumps:
|
||||
self.pumps[pump_id].initialize(drain_port, output_port, speed)
|
||||
self.pumps[pump_id].wait_until_idle()
|
||||
return True
|
||||
|
||||
target_port = port or self.port
|
||||
target_baudrate = baudrate or self.baudrate
|
||||
|
||||
try:
|
||||
self.serial_port = serial.Serial(target_port, target_baudrate, timeout=0.5)
|
||||
self._is_connected = True
|
||||
self.port = target_port
|
||||
self.baudrate = target_baudrate
|
||||
connect_allow_times = 5
|
||||
while not self.serial_port.is_open and connect_allow_times > 0:
|
||||
time.sleep(0.5)
|
||||
connect_allow_times -= 1
|
||||
print(f"尝试连接到 {target_port} @ {target_baudrate},剩余尝试次数: {connect_allow_times}", self.debug)
|
||||
raise ValueError("串口未打开,请检查设备连接")
|
||||
print(f"已连接到 {target_port} @ {target_baudrate}", self.debug)
|
||||
threading.Thread(target=self._read_data, daemon=True).start()
|
||||
return False
|
||||
|
||||
def pump_aspirate(self, pump_id: int, volume: int, valve_port: int):
|
||||
"""
|
||||
泵吸液 (阻塞)
|
||||
:param valve_port: 阀门端口 (1-8)
|
||||
"""
|
||||
pump_id = int(pump_id)
|
||||
valve_port = int(valve_port)
|
||||
if pump_id in self.pumps:
|
||||
pump = self.pumps[pump_id]
|
||||
# 1. 切换阀门
|
||||
pump.switch_valve(valve_port)
|
||||
pump.wait_until_idle()
|
||||
# 2. 吸液
|
||||
pump.aspirate(volume)
|
||||
pump.wait_until_idle()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"ChinweDevice连接失败: {e}")
|
||||
self._is_connected = False
|
||||
return False
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
return False
|
||||
|
||||
def pump_dispense(self, pump_id: int, volume: int, valve_port: int):
|
||||
"""
|
||||
断开串口连接
|
||||
|
||||
Returns:
|
||||
断开是否成功
|
||||
泵排液 (阻塞)
|
||||
:param valve_port: 阀门端口 (1-8)
|
||||
"""
|
||||
if self.serial_port and self.serial_port.is_open:
|
||||
try:
|
||||
self.serial_port.close()
|
||||
self._is_connected = False
|
||||
print("已断开串口连接")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"断开连接失败: {e}")
|
||||
return False
|
||||
pump_id = int(pump_id)
|
||||
valve_port = int(valve_port)
|
||||
if pump_id in self.pumps:
|
||||
pump = self.pumps[pump_id]
|
||||
# 1. 切换阀门
|
||||
pump.switch_valve(valve_port)
|
||||
pump.wait_until_idle()
|
||||
# 2. 排液
|
||||
pump.dispense(volume)
|
||||
pump.wait_until_idle()
|
||||
return True
|
||||
return False
|
||||
|
||||
def pump_valve(self, pump_id: int, port: int):
|
||||
"""泵切换阀门 (阻塞)"""
|
||||
pump_id = int(pump_id)
|
||||
port = int(port)
|
||||
if pump_id in self.pumps:
|
||||
pump = self.pumps[pump_id]
|
||||
pump.switch_valve(port)
|
||||
pump.wait_until_idle()
|
||||
return True
|
||||
return False
|
||||
|
||||
def motor_run_continuous(self, motor_id: int, speed: int, direction: str = "顺时针"):
|
||||
"""
|
||||
电机一直旋转 (速度模式)
|
||||
:param direction: "顺时针" or "逆时针"
|
||||
"""
|
||||
motor_id = int(motor_id)
|
||||
if motor_id not in self.motors: return False
|
||||
|
||||
dir_val = 0 if direction == "顺时针" else 1
|
||||
self.motors[motor_id].run_speed(speed, dir_val)
|
||||
return True
|
||||
|
||||
def _send_motor_command(self, command: str) -> bool:
|
||||
|
||||
def motor_rotate_quarter(self, motor_id: int, speed: int = 60, direction: str = "顺时针"):
|
||||
"""
|
||||
发送电机控制命令
|
||||
|
||||
Args:
|
||||
command: 电机命令字符串,例如 "M 1 CW 1.5"
|
||||
|
||||
Returns:
|
||||
发送是否成功
|
||||
电机旋转1/4圈 (阻塞)
|
||||
假设电机设置为 3200 脉冲/圈,1/4圈 = 800脉冲
|
||||
"""
|
||||
if not self.is_connected:
|
||||
print("设备未连接")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.serial_port.write((command + "\n").encode('utf-8'))
|
||||
print(f"发送命令: {command}")
|
||||
motor_id = int(motor_id)
|
||||
if motor_id not in self.motors: return False
|
||||
|
||||
pulses = 800
|
||||
dir_val = 0 if direction == "顺时针" else 1
|
||||
|
||||
self.motors[motor_id].run_position(pulses, speed, dir_val, absolute=False)
|
||||
|
||||
# 预估时间阻塞 (单位: 分钟 -> 秒)
|
||||
# Time(s) = revs / (RPM/60). revs = 0.25. time = 15 / RPM.
|
||||
estimated_time = 15.0 / max(1, speed)
|
||||
time.sleep(estimated_time + 0.5)
|
||||
|
||||
return True
|
||||
|
||||
def motor_stop(self, motor_id: int):
|
||||
"""电机停止"""
|
||||
motor_id = int(motor_id)
|
||||
if motor_id in self.motors:
|
||||
self.motors[motor_id].stop()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"发送命令失败: {e}")
|
||||
return False
|
||||
|
||||
def rotate_motor(self, motor_id: int, turns: float, clockwise: bool = True) -> bool:
|
||||
"""
|
||||
使电机转动指定圈数
|
||||
|
||||
Args:
|
||||
motor_id: 电机ID(1, 2, 3...)
|
||||
turns: 转动圈数,支持小数
|
||||
clockwise: True为顺时针,False为逆时针
|
||||
|
||||
Returns:
|
||||
命令发送是否成功
|
||||
"""
|
||||
if clockwise:
|
||||
command = f"M {motor_id} CW {turns}"
|
||||
else:
|
||||
command = f"M {motor_id} CCW {turns}"
|
||||
return self._send_motor_command(command)
|
||||
return False
|
||||
|
||||
def set_motor_speed(self, motor_id: int, speed: float) -> bool:
|
||||
def wait_sensor_level(self, target_state: str = "有液", timeout: int = 30) -> bool:
|
||||
"""
|
||||
设置电机转速(如果设备支持)
|
||||
|
||||
Args:
|
||||
motor_id: 电机ID(1, 2, 3...)
|
||||
speed: 转速值
|
||||
|
||||
Returns:
|
||||
命令发送是否成功
|
||||
等待传感器达到指定电平
|
||||
:param target_state: "有液" or "无液"
|
||||
"""
|
||||
command = f"M {motor_id} SPEED {speed}"
|
||||
return self._send_motor_command(command)
|
||||
target_bool = True if target_state == "有液" else False
|
||||
|
||||
def _read_data(self) -> List[str]:
|
||||
"""
|
||||
读取串口数据并解析
|
||||
|
||||
Returns:
|
||||
读取到的数据行列表
|
||||
"""
|
||||
print("开始读取串口数据...")
|
||||
if not self.is_connected:
|
||||
return []
|
||||
|
||||
data_lines = []
|
||||
try:
|
||||
while self.serial_port.in_waiting:
|
||||
time.sleep(0.1) # 等待数据稳定
|
||||
try:
|
||||
line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
|
||||
if line:
|
||||
data_lines.append(line)
|
||||
self._parse_sensor_data(line)
|
||||
except Exception as ex:
|
||||
print(f"解码数据错误: {ex}")
|
||||
except Exception as e:
|
||||
print(f"读取串口数据错误: {e}")
|
||||
|
||||
return data_lines
|
||||
|
||||
def _parse_sensor_data(self, line: str) -> None:
|
||||
"""
|
||||
解析传感器数据
|
||||
|
||||
Args:
|
||||
line: 接收到的数据行
|
||||
"""
|
||||
# 解析电源电压
|
||||
if "电源电压" in line:
|
||||
try:
|
||||
val = float(line.split(":")[1].replace("V", "").strip())
|
||||
self._voltage = val
|
||||
if self.debug:
|
||||
print(f"电源电压更新: {val}V")
|
||||
except Exception:
|
||||
pass
|
||||
self.logger.info(f"Wait sensor: {target_state} ({target_bool}), timeout: {timeout}")
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
if self.sensor_level == target_bool:
|
||||
return True
|
||||
time.sleep(0.1)
|
||||
self.logger.warning("Wait sensor level timeout")
|
||||
return False
|
||||
|
||||
# 解析电导率和ADC原始值(支持两种格式)
|
||||
if "电导率" in line and "ADC原始值" in line:
|
||||
try:
|
||||
# 支持格式如:电导率:2.50ms/cm, ADC原始值:2052
|
||||
ec_match = re.search(r"电导率[::]\s*([\d\.]+)", line)
|
||||
adc_match = re.search(r"ADC原始值[::]\s*(\d+)", line)
|
||||
if ec_match:
|
||||
ec_val = float(ec_match.group(1))
|
||||
self._ec_value = ec_val
|
||||
if self.debug:
|
||||
print(f"电导率更新: {ec_val:.2f} ms/cm")
|
||||
if adc_match:
|
||||
adc_val = int(adc_match.group(1))
|
||||
self._ec_adc_value = adc_val
|
||||
if self.debug:
|
||||
print(f"EC ADC原始值更新: {adc_val}")
|
||||
except Exception:
|
||||
pass
|
||||
# 仅电导率,无ADC原始值
|
||||
elif "电导率" in line:
|
||||
try:
|
||||
val = float(line.split(":")[1].replace("ms/cm", "").strip())
|
||||
self._ec_value = val
|
||||
if self.debug:
|
||||
print(f"电导率更新: {val:.2f} ms/cm")
|
||||
except Exception:
|
||||
pass
|
||||
# 仅ADC原始值(如有分开回传场景)
|
||||
elif "ADC原始值" in line:
|
||||
try:
|
||||
adc_val = int(line.split(":")[1].strip())
|
||||
self._ec_adc_value = adc_val
|
||||
if self.debug:
|
||||
print(f"EC ADC原始值更新: {adc_val}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def spin_when_ec_ge_0():
|
||||
pass
|
||||
|
||||
def wait_time(self, duration: int) -> bool:
|
||||
"""
|
||||
等待指定时间 (秒)
|
||||
:param duration: 秒
|
||||
"""
|
||||
self.logger.info(f"Waiting for {duration} seconds...")
|
||||
time.sleep(duration)
|
||||
return True
|
||||
|
||||
def execute_command_from_outer(self, command_dict: Dict[str, Any]) -> bool:
|
||||
"""支持标准 JSON 指令调用"""
|
||||
return super().execute_command_from_outer(command_dict)
|
||||
|
||||
def main():
|
||||
"""测试函数"""
|
||||
print("=== ChinWe设备测试 ===")
|
||||
|
||||
# 创建设备实例
|
||||
device = ChinweDevice("/dev/tty.usbserial-A5069RR4", debug=True)
|
||||
try:
|
||||
# 测试5: 发送电机命令
|
||||
print("\n5. 发送电机命令测试:")
|
||||
print(" 5.3 使用通用函数控制电机20顺时针转2圈:")
|
||||
device.rotate_motor(2, 20.0, clockwise=True)
|
||||
time.sleep(0.5)
|
||||
finally:
|
||||
time.sleep(10)
|
||||
# 测试7: 断开连接
|
||||
print("\n7. 断开连接:")
|
||||
device.disconnect()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
# Test
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
dev = ChinweDevice(port="192.168.31.201:8899")
|
||||
try:
|
||||
if dev.is_connected:
|
||||
print(f"Status: Level={dev.sensor_level}, RSSI={dev.sensor_rssi}")
|
||||
|
||||
# Test pump 1
|
||||
# dev.pump_valve(1, 1)
|
||||
# dev.pump_move(1, 1000, "aspirate")
|
||||
|
||||
# Test motor 4
|
||||
# dev.motor_run(4, 60, 0, 2)
|
||||
|
||||
for _ in range(5):
|
||||
print(f"Level={dev.sensor_level}, RSSI={dev.sensor_rssi}")
|
||||
time.sleep(1)
|
||||
finally:
|
||||
dev.disconnect()
|
||||
|
||||
93
unilabos/devices/workstation/post_process/bottle_carriers.py
Normal file
93
unilabos/devices/workstation/post_process/bottle_carriers.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
|
||||
|
||||
from unilabos.resources.itemized_carrier import BottleCarrier
|
||||
from unilabos.devices.workstation.post_process.bottles import POST_PROCESS_PolymerStation_Reagent_Bottle
|
||||
|
||||
# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 聚合站(PolymerStation)载体定义(统一入口)
|
||||
# ============================================================================
|
||||
|
||||
def POST_PROCESS_Raw_1BottleCarrier(name: str) -> BottleCarrier:
|
||||
"""聚合站-单试剂瓶载架
|
||||
|
||||
参数:
|
||||
- name: 载架名称前缀
|
||||
"""
|
||||
|
||||
# 载架尺寸 (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="POST_PROCESS_Raw_1BottleCarrier",
|
||||
)
|
||||
carrier.num_items_x = 1
|
||||
carrier.num_items_y = 1
|
||||
carrier.num_items_z = 1
|
||||
# 统一后缀采用 "flask_1" 命名(可按需调整)
|
||||
carrier[0] = POST_PROCESS_PolymerStation_Reagent_Bottle(f"{name}_flask_1")
|
||||
return carrier
|
||||
|
||||
def POST_PROCESS_Reaction_1BottleCarrier(name: str) -> BottleCarrier:
|
||||
"""聚合站-单试剂瓶载架
|
||||
|
||||
参数:
|
||||
- name: 载架名称前缀
|
||||
"""
|
||||
|
||||
# 载架尺寸 (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="POST_PROCESS_Reaction_1BottleCarrier",
|
||||
)
|
||||
carrier.num_items_x = 1
|
||||
carrier.num_items_y = 1
|
||||
carrier.num_items_z = 1
|
||||
# 统一后缀采用 "flask_1" 命名(可按需调整)
|
||||
carrier[0] = POST_PROCESS_PolymerStation_Reagent_Bottle(f"{name}_flask_1")
|
||||
return carrier
|
||||
20
unilabos/devices/workstation/post_process/bottles.py
Normal file
20
unilabos/devices/workstation/post_process/bottles.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from unilabos.resources.itemized_carrier import Bottle
|
||||
|
||||
|
||||
def POST_PROCESS_PolymerStation_Reagent_Bottle(
|
||||
name: str,
|
||||
diameter: float = 70.0,
|
||||
height: float = 120.0,
|
||||
max_volume: float = 500000.0, # 500mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建试剂瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="POST_PROCESS_PolymerStation_Reagent_Bottle",
|
||||
)
|
||||
|
||||
46
unilabos/devices/workstation/post_process/decks.py
Normal file
46
unilabos/devices/workstation/post_process/decks.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from os import name
|
||||
from pylabrobot.resources import Deck, Coordinate, Rotation
|
||||
|
||||
from unilabos.devices.workstation.post_process.warehouses import (
|
||||
post_process_warehouse_4x3x1,
|
||||
post_process_warehouse_4x3x1_2,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class post_process_deck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "post_process_deck",
|
||||
size_x: float = 2000.0,
|
||||
size_y: float = 1000.0,
|
||||
size_z: float = 2670.0,
|
||||
category: str = "deck",
|
||||
setup: bool = True,
|
||||
) -> None:
|
||||
super().__init__(name=name, size_x=1700.0, size_y=1350.0, size_z=2670.0)
|
||||
if setup:
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
# 添加仓库
|
||||
self.warehouses = {
|
||||
"原料罐堆栈": post_process_warehouse_4x3x1("原料罐堆栈"),
|
||||
"反应罐堆栈": post_process_warehouse_4x3x1_2("反应罐堆栈"),
|
||||
|
||||
}
|
||||
# warehouse 的位置
|
||||
self.warehouse_locations = {
|
||||
"原料罐堆栈": Coordinate(350.0, 55.0, 0.0),
|
||||
"反应罐堆栈": Coordinate(1000.0, 55.0, 0.0),
|
||||
|
||||
}
|
||||
|
||||
for warehouse_name, warehouse in self.warehouses.items():
|
||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
157
unilabos/devices/workstation/post_process/opcua_huairou.json
Normal file
157
unilabos/devices/workstation/post_process/opcua_huairou.json
Normal file
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"register_node_list_from_csv_path": {
|
||||
"path": "opcua_nodes_huairou.csv"
|
||||
},
|
||||
"create_flow": [
|
||||
{
|
||||
"name": "trigger_grab_action",
|
||||
"description": "触发反应罐及原料罐抓取动作",
|
||||
"parameters": ["reaction_tank_number", "raw_tank_number"],
|
||||
"action": [
|
||||
{
|
||||
"init_function": {
|
||||
"func_name": "init_grab_params",
|
||||
"write_nodes": ["reaction_tank_number", "raw_tank_number"]
|
||||
},
|
||||
"start_function": {
|
||||
"func_name": "start_grab",
|
||||
"write_nodes": {"grab_trigger": true},
|
||||
"condition_nodes": ["grab_complete"],
|
||||
"stop_condition_expression": "grab_complete == True",
|
||||
"timeout_seconds": 999999.0
|
||||
},
|
||||
"stop_function": {
|
||||
"func_name": "stop_grab",
|
||||
"write_nodes": {"grab_trigger": false}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "trigger_post_processing",
|
||||
"description": "触发后处理动作",
|
||||
"parameters": ["atomization_fast_speed", "wash_slow_speed","injection_pump_suction_speed",
|
||||
"injection_pump_push_speed","raw_liquid_suction_count","first_wash_water_amount",
|
||||
"second_wash_water_amount","first_powder_mixing_time","second_powder_mixing_time",
|
||||
"first_powder_wash_count","second_powder_wash_count","initial_water_amount",
|
||||
"pre_filtration_mixing_time","atomization_pressure_kpa"],
|
||||
"action": [
|
||||
{
|
||||
"init_function": {
|
||||
"func_name": "init_post_processing_params",
|
||||
"write_nodes": ["atomization_fast_speed", "wash_slow_speed","injection_pump_suction_speed",
|
||||
"injection_pump_push_speed","raw_liquid_suction_count","first_wash_water_amount",
|
||||
"second_wash_water_amount","first_powder_mixing_time","second_powder_mixing_time",
|
||||
"first_powder_wash_count","second_powder_wash_count","initial_water_amount",
|
||||
"pre_filtration_mixing_time","atomization_pressure_kpa"]
|
||||
},
|
||||
"start_function": {
|
||||
"func_name": "start_post_processing",
|
||||
"write_nodes": {"post_process_trigger": true},
|
||||
"condition_nodes": ["post_process_complete"],
|
||||
"stop_condition_expression": "post_process_complete == True",
|
||||
"timeout_seconds": 999999.0
|
||||
},
|
||||
"stop_function": {
|
||||
"func_name": "stop_post_processing",
|
||||
"write_nodes": {"post_process_trigger": false}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "trigger_cleaning_action",
|
||||
"description": "触发清洗及管路吹气动作",
|
||||
"parameters": ["nmp_outer_wall_cleaning_injection", "nmp_outer_wall_cleaning_count","nmp_outer_wall_cleaning_wait_time",
|
||||
"nmp_outer_wall_cleaning_waste_time","nmp_inner_wall_cleaning_injection","nmp_inner_wall_cleaning_count",
|
||||
"nmp_pump_cleaning_suction_count",
|
||||
"nmp_inner_wall_cleaning_waste_time",
|
||||
"nmp_stirrer_cleaning_injection",
|
||||
"nmp_stirrer_cleaning_count",
|
||||
"nmp_stirrer_cleaning_wait_time",
|
||||
"nmp_stirrer_cleaning_waste_time",
|
||||
"water_outer_wall_cleaning_injection",
|
||||
"water_outer_wall_cleaning_count",
|
||||
"water_outer_wall_cleaning_wait_time",
|
||||
"water_outer_wall_cleaning_waste_time",
|
||||
"water_inner_wall_cleaning_injection",
|
||||
"water_inner_wall_cleaning_count",
|
||||
"water_pump_cleaning_suction_count",
|
||||
"water_inner_wall_cleaning_waste_time",
|
||||
"water_stirrer_cleaning_injection",
|
||||
"water_stirrer_cleaning_count",
|
||||
"water_stirrer_cleaning_wait_time",
|
||||
"water_stirrer_cleaning_waste_time",
|
||||
"acetone_outer_wall_cleaning_injection",
|
||||
"acetone_outer_wall_cleaning_count",
|
||||
"acetone_outer_wall_cleaning_wait_time",
|
||||
"acetone_outer_wall_cleaning_waste_time",
|
||||
"acetone_inner_wall_cleaning_injection",
|
||||
"acetone_inner_wall_cleaning_count",
|
||||
"acetone_pump_cleaning_suction_count",
|
||||
"acetone_inner_wall_cleaning_waste_time",
|
||||
"acetone_stirrer_cleaning_injection",
|
||||
"acetone_stirrer_cleaning_count",
|
||||
"acetone_stirrer_cleaning_wait_time",
|
||||
"acetone_stirrer_cleaning_waste_time",
|
||||
"pipe_blowing_time",
|
||||
"injection_pump_forward_empty_suction_count",
|
||||
"injection_pump_reverse_empty_suction_count",
|
||||
"filtration_liquid_selection"],
|
||||
"action": [
|
||||
{
|
||||
"init_function": {
|
||||
"func_name": "init_cleaning_params",
|
||||
"write_nodes": ["nmp_outer_wall_cleaning_injection", "nmp_outer_wall_cleaning_count","nmp_outer_wall_cleaning_wait_time",
|
||||
"nmp_outer_wall_cleaning_waste_time","nmp_inner_wall_cleaning_injection","nmp_inner_wall_cleaning_count",
|
||||
"nmp_pump_cleaning_suction_count",
|
||||
"nmp_inner_wall_cleaning_waste_time",
|
||||
"nmp_stirrer_cleaning_injection",
|
||||
"nmp_stirrer_cleaning_count",
|
||||
"nmp_stirrer_cleaning_wait_time",
|
||||
"nmp_stirrer_cleaning_waste_time",
|
||||
"water_outer_wall_cleaning_injection",
|
||||
"water_outer_wall_cleaning_count",
|
||||
"water_outer_wall_cleaning_wait_time",
|
||||
"water_outer_wall_cleaning_waste_time",
|
||||
"water_inner_wall_cleaning_injection",
|
||||
"water_inner_wall_cleaning_count",
|
||||
"water_pump_cleaning_suction_count",
|
||||
"water_inner_wall_cleaning_waste_time",
|
||||
"water_stirrer_cleaning_injection",
|
||||
"water_stirrer_cleaning_count",
|
||||
"water_stirrer_cleaning_wait_time",
|
||||
"water_stirrer_cleaning_waste_time",
|
||||
"acetone_outer_wall_cleaning_injection",
|
||||
"acetone_outer_wall_cleaning_count",
|
||||
"acetone_outer_wall_cleaning_wait_time",
|
||||
"acetone_outer_wall_cleaning_waste_time",
|
||||
"acetone_inner_wall_cleaning_injection",
|
||||
"acetone_inner_wall_cleaning_count",
|
||||
"acetone_pump_cleaning_suction_count",
|
||||
"acetone_inner_wall_cleaning_waste_time",
|
||||
"acetone_stirrer_cleaning_injection",
|
||||
"acetone_stirrer_cleaning_count",
|
||||
"acetone_stirrer_cleaning_wait_time",
|
||||
"acetone_stirrer_cleaning_waste_time",
|
||||
"pipe_blowing_time",
|
||||
"injection_pump_forward_empty_suction_count",
|
||||
"injection_pump_reverse_empty_suction_count",
|
||||
"filtration_liquid_selection"]
|
||||
},
|
||||
"start_function": {
|
||||
"func_name": "start_cleaning",
|
||||
"write_nodes": {"cleaning_and_pipe_blowing_trigger": true},
|
||||
"condition_nodes": ["cleaning_complete"],
|
||||
"stop_condition_expression": "cleaning_complete == True",
|
||||
"timeout_seconds": 999999.0
|
||||
},
|
||||
"stop_function": {
|
||||
"func_name": "stop_cleaning",
|
||||
"write_nodes": {"cleaning_and_pipe_blowing_trigger": false}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
Name,EnglishName,NodeType,DataType,NodeLanguage,NodeId
|
||||
原料罐号码,raw_tank_number,VARIABLE,INT16,Chinese,ns=4;s=OPC|原料罐号码
|
||||
反应罐号码,reaction_tank_number,VARIABLE,INT16,Chinese,ns=4;s=OPC|反应罐号码
|
||||
反应罐及原料罐抓取触发,grab_trigger,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|反应罐及原料罐抓取触发
|
||||
后处理动作触发,post_process_trigger,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|后处理动作触发
|
||||
搅拌桨雾化快速,atomization_fast_speed,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|搅拌桨雾化快速
|
||||
搅拌桨洗涤慢速,wash_slow_speed,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|搅拌桨洗涤慢速
|
||||
注射泵抽液速度,injection_pump_suction_speed,VARIABLE,INT16,Chinese,ns=4;s=OPC|注射泵抽液速度
|
||||
注射泵推液速度,injection_pump_push_speed,VARIABLE,INT16,Chinese,ns=4;s=OPC|注射泵推液速度
|
||||
抽原液次数,raw_liquid_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|抽原液次数
|
||||
第1次洗涤加水量,first_wash_water_amount,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|第1次洗涤加水量
|
||||
第2次洗涤加水量,second_wash_water_amount,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|第2次洗涤加水量
|
||||
第1次粉末搅拌时间,first_powder_mixing_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|第1次粉末搅拌时间
|
||||
第2次粉末搅拌时间,second_powder_mixing_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|第2次粉末搅拌时间
|
||||
第1次粉末洗涤次数,first_powder_wash_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|第1次粉末洗涤次数
|
||||
第2次粉末洗涤次数,second_powder_wash_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|第2次粉末洗涤次数
|
||||
最开始加水量,initial_water_amount,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|最开始加水量
|
||||
抽滤前搅拌时间,pre_filtration_mixing_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|抽滤前搅拌时间
|
||||
雾化压力Kpa,atomization_pressure_kpa,VARIABLE,INT16,Chinese,ns=4;s=OPC|雾化压力Kpa
|
||||
清洗及管路吹气触发,cleaning_and_pipe_blowing_trigger,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|清洗及管路吹气触发
|
||||
废液桶满报警,waste_tank_full_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|废液桶满报警
|
||||
清水桶空报警,water_tank_empty_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|清水桶空报警
|
||||
NMP桶空报警,nmp_tank_empty_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|NMP桶空报警
|
||||
丙酮桶空报警,acetone_tank_empty_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|丙酮桶空报警
|
||||
门开报警,door_open_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|门开报警
|
||||
反应罐及原料罐抓取完成PLCtoPC,grab_complete,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|反应罐及原料罐抓取完成PLCtoPC
|
||||
后处理动作完成PLCtoPC,post_process_complete,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|后处理动作完成PLCtoPC
|
||||
清洗及管路吹气完成PLCtoPC,cleaning_complete,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|清洗及管路吹气完成PLCtoPC
|
||||
远程模式PLCtoPC,remote_mode,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|远程模式PLCtoPC
|
||||
设备准备就绪PLCtoPC,device_ready,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|设备准备就绪PLCtoPC
|
||||
NMP外壁清洗加注,nmp_outer_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|NMP外壁清洗加注
|
||||
NMP外壁清洗次数,nmp_outer_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|NMP外壁清洗次数
|
||||
NMP外壁清洗等待时间,nmp_outer_wall_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP外壁清洗等待时间
|
||||
NMP外壁清洗抽废时间,nmp_outer_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP外壁清洗抽废时间
|
||||
NMP内壁清洗加注,nmp_inner_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|NMP内壁清洗加注
|
||||
NMP内壁清洗次数,nmp_inner_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|NMP内壁清洗次数
|
||||
NMP泵清洗抽次数,nmp_pump_cleaning_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|NMP泵清洗抽次数
|
||||
NMP内壁清洗抽废时间,nmp_inner_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP内壁清洗抽废时间
|
||||
NMP搅拌桨清洗加注,nmp_stirrer_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|NMP搅拌桨清洗加注
|
||||
NMP搅拌桨清洗次数,nmp_stirrer_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|NMP搅拌桨清洗次数
|
||||
NMP搅拌桨清洗等待时间,nmp_stirrer_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP搅拌桨清洗等待时间
|
||||
NMP搅拌桨清洗抽废时间,nmp_stirrer_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP搅拌桨清洗抽废时间
|
||||
清水外壁清洗加注,water_outer_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|清水外壁清洗加注
|
||||
清水外壁清洗次数,water_outer_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|清水外壁清洗次数
|
||||
清水外壁清洗等待时间,water_outer_wall_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水外壁清洗等待时间
|
||||
清水外壁清洗抽废时间,water_outer_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水外壁清洗抽废时间
|
||||
清水内壁清洗加注,water_inner_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|清水内壁清洗加注
|
||||
清水内壁清洗次数,water_inner_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|清水内壁清洗次数
|
||||
清水泵清洗抽次数,water_pump_cleaning_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|清水泵清洗抽次数
|
||||
清水内壁清洗抽废时间,water_inner_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水内壁清洗抽废时间
|
||||
清水搅拌桨清洗加注,water_stirrer_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|清水搅拌桨清洗加注
|
||||
清水搅拌桨清洗次数,water_stirrer_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|清水搅拌桨清洗次数
|
||||
清水搅拌桨清洗等待时间,water_stirrer_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水搅拌桨清洗等待时间
|
||||
清水搅拌桨清洗抽废时间,water_stirrer_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水搅拌桨清洗抽废时间
|
||||
丙酮外壁清洗加注,acetone_outer_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|丙酮外壁清洗加注
|
||||
丙酮外壁清洗次数,acetone_outer_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|丙酮外壁清洗次数
|
||||
丙酮外壁清洗等待时间,acetone_outer_wall_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮外壁清洗等待时间
|
||||
丙酮外壁清洗抽废时间,acetone_outer_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮外壁清洗抽废时间
|
||||
丙酮内壁清洗加注,acetone_inner_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|丙酮内壁清洗加注
|
||||
丙酮内壁清洗次数,acetone_inner_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|丙酮内壁清洗次数
|
||||
丙酮泵清洗抽次数,acetone_pump_cleaning_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|丙酮泵清洗抽次数
|
||||
丙酮内壁清洗抽废时间,acetone_inner_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮内壁清洗抽废时间
|
||||
丙酮搅拌桨清洗加注,acetone_stirrer_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|丙酮搅拌桨清洗加注
|
||||
丙酮搅拌桨清洗次数,acetone_stirrer_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|丙酮搅拌桨清洗次数
|
||||
丙酮搅拌桨清洗等待时间,acetone_stirrer_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮搅拌桨清洗等待时间
|
||||
丙酮搅拌桨清洗抽废时间,acetone_stirrer_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮搅拌桨清洗抽废时间
|
||||
管道吹气时间,pipe_blowing_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|管道吹气时间
|
||||
注射泵正向空抽次数,injection_pump_forward_empty_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|注射泵正向空抽次数
|
||||
注射泵反向空抽次数,injection_pump_reverse_empty_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|注射泵反向空抽次数
|
||||
抽滤液选择0水1丙酮,filtration_liquid_selection,VARIABLE,INT16,Chinese,ns=4;s=OPC|抽滤液选择0水1丙酮
|
||||
|
1781
unilabos/devices/workstation/post_process/post_process.py
Normal file
1781
unilabos/devices/workstation/post_process/post_process.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "post_process_station",
|
||||
"name": "post_process_station",
|
||||
"children": [
|
||||
"post_process_deck"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "post_process_station",
|
||||
"config": {
|
||||
"url": "opc.tcp://LAPTOP-AN6QGCSD:53530/OPCUA/SimulationServer",
|
||||
"config_path": "C:\\Users\\Roy\\Desktop\\DPLC\\Uni-Lab-OS\\unilabos\\devices\\workstation\\post_process\\opcua_huairou.json",
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "post_process_deck",
|
||||
"_resource_type": "unilabos.devices.workstation.post_process.decks:post_process_deck"
|
||||
}
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "post_process_deck",
|
||||
"name": "post_process_deck",
|
||||
"sample_id": null,
|
||||
"children": [],
|
||||
"parent": "post_process_station",
|
||||
"type": "deck",
|
||||
"class": "post_process_deck",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "post_process_deck",
|
||||
"setup": true
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
from typing import Dict, Optional, List, Union
|
||||
from pylabrobot.resources import Coordinate
|
||||
from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources
|
||||
|
||||
from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR
|
||||
|
||||
|
||||
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
|
||||
def warehouse_factory(
|
||||
name: str,
|
||||
num_items_x: int = 1,
|
||||
num_items_y: int = 4,
|
||||
num_items_z: int = 4,
|
||||
dx: float = 137.0,
|
||||
dy: float = 96.0,
|
||||
dz: float = 120.0,
|
||||
item_dx: float = 10.0,
|
||||
item_dy: float = 10.0,
|
||||
item_dz: float = 10.0,
|
||||
resource_size_x: float = 127.0,
|
||||
resource_size_y: float = 86.0,
|
||||
resource_size_z: float = 25.0,
|
||||
removed_positions: Optional[List[int]] = None,
|
||||
empty: bool = False,
|
||||
category: str = "warehouse",
|
||||
model: Optional[str] = None,
|
||||
col_offset: int = 0, # 列起始偏移量,用于生成5-8等命名
|
||||
layout: str = "col-major", # 新增:排序方式,"col-major"=列优先,"row-major"=行优先
|
||||
):
|
||||
# 创建位置坐标
|
||||
locations = []
|
||||
|
||||
for layer in range(num_items_z): # 层
|
||||
for row in range(num_items_y): # 行
|
||||
for col in range(num_items_x): # 列
|
||||
# 计算位置
|
||||
x = dx + col * item_dx
|
||||
|
||||
# 根据 layout 决定 y 坐标计算
|
||||
if layout == "row-major":
|
||||
# 行优先:row=0(第1行) 应该显示在上方,y 值最小
|
||||
y = dy + row * item_dy
|
||||
else:
|
||||
# 列优先:保持原逻辑
|
||||
y = dy + (num_items_y - row - 1) * item_dy
|
||||
|
||||
z = dz + (num_items_z - layer - 1) * item_dz
|
||||
locations.append(Coordinate(x, y, z))
|
||||
|
||||
if removed_positions:
|
||||
locations = [loc for i, loc in enumerate(locations) if i not in removed_positions]
|
||||
|
||||
_sites = create_homogeneous_resources(
|
||||
klass=ResourceHolder,
|
||||
locations=locations,
|
||||
resource_size_x=resource_size_x,
|
||||
resource_size_y=resource_size_y,
|
||||
resource_size_z=resource_size_z,
|
||||
name_prefix=name,
|
||||
)
|
||||
|
||||
len_x, len_y = (num_items_x, num_items_y) if num_items_z == 1 else (num_items_y, num_items_z) if num_items_x == 1 else (num_items_x, num_items_z)
|
||||
|
||||
# 🔑 修改:使用数字命名,最上面是4321,最下面是12,11,10,9
|
||||
# 命名顺序必须与坐标生成顺序一致:层 → 行 → 列
|
||||
keys = []
|
||||
for layer in range(num_items_z): # 遍历每一层
|
||||
for row in range(num_items_y): # 遍历每一行
|
||||
for col in range(num_items_x): # 遍历每一列
|
||||
# 倒序计算全局行号:row=0 应该对应 global_row=0(第1行:4321)
|
||||
# row=1 应该对应 global_row=1(第2行:8765)
|
||||
# row=2 应该对应 global_row=2(第3行:12,11,10,9)
|
||||
# 但前端显示时 row=2 在最上面,所以需要反转
|
||||
reversed_row = (num_items_y - 1 - row) # row=0→reversed_row=2, row=1→reversed_row=1, row=2→reversed_row=0
|
||||
global_row = layer * num_items_y + reversed_row
|
||||
|
||||
# 每行的最大数字 = (global_row + 1) * num_items_x + col_offset
|
||||
base_num = (global_row + 1) * num_items_x + col_offset
|
||||
|
||||
# 从右到左递减:4,3,2,1
|
||||
key = str(base_num - col)
|
||||
keys.append(key)
|
||||
|
||||
sites = {i: site for i, site in zip(keys, _sites.values())}
|
||||
|
||||
return WareHouse(
|
||||
name=name,
|
||||
size_x=dx + item_dx * num_items_x,
|
||||
size_y=dy + item_dy * num_items_y,
|
||||
size_z=dz + item_dz * num_items_z,
|
||||
num_items_x = num_items_x,
|
||||
num_items_y = num_items_y,
|
||||
num_items_z = num_items_z,
|
||||
ordering_layout=layout, # 传递排序方式到 ordering_layout
|
||||
sites=sites,
|
||||
category=category,
|
||||
model=model,
|
||||
)
|
||||
|
||||
|
||||
class WareHouse(ItemizedCarrier):
|
||||
"""堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)"""
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size_x: float,
|
||||
size_y: float,
|
||||
size_z: float,
|
||||
num_items_x: int,
|
||||
num_items_y: int,
|
||||
num_items_z: int,
|
||||
layout: str = "x-y",
|
||||
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
||||
category: str = "warehouse",
|
||||
model: Optional[str] = None,
|
||||
ordering_layout: str = "col-major",
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
name=name,
|
||||
size_x=size_x,
|
||||
size_y=size_y,
|
||||
size_z=size_z,
|
||||
# ordered_items=ordered_items,
|
||||
# ordering=ordering,
|
||||
num_items_x=num_items_x,
|
||||
num_items_y=num_items_y,
|
||||
num_items_z=num_items_z,
|
||||
layout=layout,
|
||||
sites=sites,
|
||||
category=category,
|
||||
model=model,
|
||||
)
|
||||
|
||||
# 保存排序方式,供graphio.py的坐标映射使用
|
||||
# 使用独立属性避免与父类的layout冲突
|
||||
self.ordering_layout = ordering_layout
|
||||
|
||||
def serialize(self) -> dict:
|
||||
"""序列化时保存 ordering_layout 属性"""
|
||||
data = super().serialize()
|
||||
data['ordering_layout'] = self.ordering_layout
|
||||
return data
|
||||
|
||||
def get_site_by_layer_position(self, row: int, col: int, layer: int) -> ResourceHolder:
|
||||
if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1):
|
||||
raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col))
|
||||
|
||||
site_index = layer * 4 + row * 1 + col
|
||||
return self.sites[site_index]
|
||||
|
||||
def add_rack_to_position(self, row: int, col: int, layer: int, rack) -> None:
|
||||
site = self.get_site_by_layer_position(row, col, layer)
|
||||
site.assign_child_resource(rack)
|
||||
|
||||
def get_rack_at_position(self, row: int, col: int, layer: int):
|
||||
site = self.get_site_by_layer_position(row, col, layer)
|
||||
return site.resource
|
||||
38
unilabos/devices/workstation/post_process/warehouses.py
Normal file
38
unilabos/devices/workstation/post_process/warehouses.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from unilabos.devices.workstation.post_process.post_process_warehouse import WareHouse, warehouse_factory
|
||||
|
||||
|
||||
|
||||
# =================== Other ===================
|
||||
|
||||
|
||||
def post_process_warehouse_4x3x1(name: str) -> WareHouse:
|
||||
"""创建post_process 4x3x1仓库"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=4,
|
||||
num_items_y=3,
|
||||
num_items_z=1,
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=137.0,
|
||||
item_dy=96.0,
|
||||
item_dz=120.0,
|
||||
category="warehouse",
|
||||
)
|
||||
|
||||
def post_process_warehouse_4x3x1_2(name: str) -> WareHouse:
|
||||
"""已弃用:创建post_process 4x3x1仓库"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=4,
|
||||
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",
|
||||
)
|
||||
@@ -174,35 +174,6 @@ bioyond_dispensing_station:
|
||||
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: {}
|
||||
|
||||
105
unilabos/registry/devices/cameraSII.yaml
Normal file
105
unilabos/registry/devices/cameraSII.yaml
Normal file
@@ -0,0 +1,105 @@
|
||||
cameracontroller_device:
|
||||
category:
|
||||
- cameraSII
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-start:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
config: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
config:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: start参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-stop:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: stop参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.cameraSII.cameraUSB:CameraController
|
||||
status_types:
|
||||
status: dict
|
||||
type: python
|
||||
config_info: []
|
||||
description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ)
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
audio_bitrate:
|
||||
default: 64k
|
||||
type: string
|
||||
audio_device:
|
||||
type: string
|
||||
fps:
|
||||
default: 30
|
||||
type: integer
|
||||
height:
|
||||
default: 720
|
||||
type: integer
|
||||
host_id:
|
||||
default: demo-host
|
||||
type: string
|
||||
rtmp_url:
|
||||
default: rtmp://srs.sciol.ac.cn:4499/live/camera-01
|
||||
type: string
|
||||
signal_backend_url:
|
||||
default: wss://sciol.ac.cn/api/realtime/signal/host
|
||||
type: string
|
||||
video_bitrate:
|
||||
default: 1500k
|
||||
type: string
|
||||
video_device:
|
||||
default: /dev/video0
|
||||
type: string
|
||||
webrtc_api:
|
||||
default: https://srs.sciol.ac.cn/rtc/v1/play/
|
||||
type: string
|
||||
webrtc_stream_url:
|
||||
default: webrtc://srs.sciol.ac.cn:4500/live/camera-01
|
||||
type: string
|
||||
width:
|
||||
default: 1280
|
||||
type: integer
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
registry_type: device
|
||||
version: 1.0.0
|
||||
344
unilabos/registry/devices/chinwe.yaml
Normal file
344
unilabos/registry/devices/chinwe.yaml
Normal file
@@ -0,0 +1,344 @@
|
||||
separator.chinwe:
|
||||
category:
|
||||
- separator
|
||||
- chinwe
|
||||
class:
|
||||
action_value_mappings:
|
||||
motor_rotate_quarter:
|
||||
goal:
|
||||
direction: 顺时针
|
||||
motor_id: 4
|
||||
speed: 60
|
||||
handles: {}
|
||||
schema:
|
||||
description: 电机旋转 1/4 圈
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
direction:
|
||||
default: 顺时针
|
||||
description: 旋转方向
|
||||
enum:
|
||||
- 顺时针
|
||||
- 逆时针
|
||||
type: string
|
||||
motor_id:
|
||||
default: '4'
|
||||
description: 选择电机 (4:搅拌, 5:旋钮)
|
||||
enum:
|
||||
- '4'
|
||||
- '5'
|
||||
type: string
|
||||
speed:
|
||||
default: 60
|
||||
description: 速度 (RPM)
|
||||
type: integer
|
||||
required:
|
||||
- motor_id
|
||||
- speed
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
motor_run_continuous:
|
||||
goal:
|
||||
direction: 顺时针
|
||||
motor_id: 4
|
||||
speed: 60
|
||||
handles: {}
|
||||
schema:
|
||||
description: 电机一直旋转 (速度模式)
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
direction:
|
||||
default: 顺时针
|
||||
description: 旋转方向
|
||||
enum:
|
||||
- 顺时针
|
||||
- 逆时针
|
||||
type: string
|
||||
motor_id:
|
||||
default: '4'
|
||||
description: 选择电机 (4:搅拌, 5:旋钮)
|
||||
enum:
|
||||
- '4'
|
||||
- '5'
|
||||
type: string
|
||||
speed:
|
||||
default: 60
|
||||
description: 速度 (RPM)
|
||||
type: integer
|
||||
required:
|
||||
- motor_id
|
||||
- speed
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
motor_stop:
|
||||
goal:
|
||||
motor_id: 4
|
||||
handles: {}
|
||||
schema:
|
||||
description: 停止指定步进电机
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
motor_id:
|
||||
default: '4'
|
||||
description: 选择电机
|
||||
enum:
|
||||
- '4'
|
||||
- '5'
|
||||
title: '注: 4=搅拌, 5=旋钮'
|
||||
type: string
|
||||
required:
|
||||
- motor_id
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pump_aspirate:
|
||||
goal:
|
||||
pump_id: 1
|
||||
valve_port: 1
|
||||
volume: 1000
|
||||
handles: {}
|
||||
schema:
|
||||
description: 注射泵吸液
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
pump_id:
|
||||
default: '1'
|
||||
description: 选择泵
|
||||
enum:
|
||||
- '1'
|
||||
- '2'
|
||||
- '3'
|
||||
type: string
|
||||
valve_port:
|
||||
default: '1'
|
||||
description: 阀门端口
|
||||
enum:
|
||||
- '1'
|
||||
- '2'
|
||||
- '3'
|
||||
- '4'
|
||||
- '5'
|
||||
- '6'
|
||||
- '7'
|
||||
- '8'
|
||||
type: string
|
||||
volume:
|
||||
default: 1000
|
||||
description: 吸液步数
|
||||
type: integer
|
||||
required:
|
||||
- pump_id
|
||||
- volume
|
||||
- valve_port
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pump_dispense:
|
||||
goal:
|
||||
pump_id: 1
|
||||
valve_port: 1
|
||||
volume: 1000
|
||||
handles: {}
|
||||
schema:
|
||||
description: 注射泵排液
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
pump_id:
|
||||
default: '1'
|
||||
description: 选择泵
|
||||
enum:
|
||||
- '1'
|
||||
- '2'
|
||||
- '3'
|
||||
type: string
|
||||
valve_port:
|
||||
default: '1'
|
||||
description: 阀门端口
|
||||
enum:
|
||||
- '1'
|
||||
- '2'
|
||||
- '3'
|
||||
- '4'
|
||||
- '5'
|
||||
- '6'
|
||||
- '7'
|
||||
- '8'
|
||||
type: string
|
||||
volume:
|
||||
default: 1000
|
||||
description: 排液步数
|
||||
type: integer
|
||||
required:
|
||||
- pump_id
|
||||
- volume
|
||||
- valve_port
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pump_initialize:
|
||||
goal:
|
||||
drain_port: 0
|
||||
output_port: 0
|
||||
pump_id: 1
|
||||
speed: 10
|
||||
handles: {}
|
||||
schema:
|
||||
description: 初始化指定注射泵
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
drain_port:
|
||||
default: 0
|
||||
description: 排液口索引
|
||||
type: integer
|
||||
output_port:
|
||||
default: 0
|
||||
description: 输出口索引
|
||||
type: integer
|
||||
pump_id:
|
||||
default: '1'
|
||||
description: 选择泵
|
||||
enum:
|
||||
- '1'
|
||||
- '2'
|
||||
- '3'
|
||||
title: '注: 1号泵, 2号泵, 3号泵'
|
||||
type: string
|
||||
speed:
|
||||
default: 10
|
||||
description: 运动速度
|
||||
type: integer
|
||||
required:
|
||||
- pump_id
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
pump_valve:
|
||||
goal:
|
||||
port: 1
|
||||
pump_id: 1
|
||||
handles: {}
|
||||
schema:
|
||||
description: 切换指定泵的阀门端口
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
port:
|
||||
default: '1'
|
||||
description: 阀门端口号 (1-8)
|
||||
enum:
|
||||
- '1'
|
||||
- '2'
|
||||
- '3'
|
||||
- '4'
|
||||
- '5'
|
||||
- '6'
|
||||
- '7'
|
||||
- '8'
|
||||
type: string
|
||||
pump_id:
|
||||
default: '1'
|
||||
description: 选择泵
|
||||
enum:
|
||||
- '1'
|
||||
- '2'
|
||||
- '3'
|
||||
type: string
|
||||
required:
|
||||
- pump_id
|
||||
- port
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
wait_sensor_level:
|
||||
goal:
|
||||
target_state: 有液
|
||||
timeout: 30
|
||||
handles: {}
|
||||
schema:
|
||||
description: 等待传感器液位条件
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
target_state:
|
||||
default: 有液
|
||||
description: 目标液位状态
|
||||
enum:
|
||||
- 有液
|
||||
- 无液
|
||||
type: string
|
||||
timeout:
|
||||
default: 30
|
||||
description: 超时时间 (秒)
|
||||
type: integer
|
||||
required:
|
||||
- target_state
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
wait_time:
|
||||
goal:
|
||||
duration: 10
|
||||
handles: {}
|
||||
schema:
|
||||
description: 等待指定时间
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
duration:
|
||||
default: 10
|
||||
description: 等待时间 (秒)
|
||||
type: integer
|
||||
required:
|
||||
- duration
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.separator.chinwe:ChinweDevice
|
||||
status_types:
|
||||
is_connected: bool
|
||||
sensor_level: bool
|
||||
sensor_rssi: int
|
||||
type: python
|
||||
config_info: []
|
||||
description: ChinWe 简易工作站控制器 (3泵, 2电机, 1传感器)
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
goal:
|
||||
baudrate:
|
||||
default: 9600
|
||||
description: 串口波特率
|
||||
type: integer
|
||||
motor_ids:
|
||||
default:
|
||||
- 4
|
||||
- 5
|
||||
description: 步进电机ID列表
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
port:
|
||||
default: 192.168.1.200:8899
|
||||
description: 串口号或 IP:Port
|
||||
type: string
|
||||
pump_ids:
|
||||
default:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
description: 注射泵ID列表
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
sensor_id:
|
||||
default: 6
|
||||
description: XKC传感器ID
|
||||
type: integer
|
||||
sensor_threshold:
|
||||
default: 300
|
||||
description: 传感器液位判定阈值
|
||||
type: integer
|
||||
timeout:
|
||||
default: 10
|
||||
description: 通信超时时间 (秒)
|
||||
type: integer
|
||||
version: 2.1.0
|
||||
@@ -9744,7 +9744,34 @@ liquid_handler.prcxi:
|
||||
touch_tip: false
|
||||
use_channels:
|
||||
- 0
|
||||
handles: {}
|
||||
handles:
|
||||
input:
|
||||
- data_key: liquid
|
||||
data_source: handle
|
||||
data_type: resource
|
||||
handler_key: sources
|
||||
label: sources
|
||||
- data_key: liquid
|
||||
data_source: executor
|
||||
data_type: resource
|
||||
handler_key: targets
|
||||
label: targets
|
||||
- data_key: liquid
|
||||
data_source: executor
|
||||
data_type: resource
|
||||
handler_key: tip_rack
|
||||
label: tip_rack
|
||||
output:
|
||||
- data_key: liquid
|
||||
data_source: handle
|
||||
data_type: resource
|
||||
handler_key: sources_out
|
||||
label: sources
|
||||
- data_key: liquid
|
||||
data_source: executor
|
||||
data_type: resource
|
||||
handler_key: targets_out
|
||||
label: targets
|
||||
placeholder_keys:
|
||||
sources: unilabos_resources
|
||||
targets: unilabos_resources
|
||||
|
||||
@@ -1,73 +1,40 @@
|
||||
neware_battery_test_system:
|
||||
category:
|
||||
- neware_battery_test_system
|
||||
- neware
|
||||
- battery_test
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-post_init:
|
||||
debug_resource_names:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
result:
|
||||
return_info: return_info
|
||||
success: success
|
||||
schema:
|
||||
description: ''
|
||||
description: 调试方法:显示所有资源的实际名称
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
ros_node:
|
||||
return_info:
|
||||
description: 资源调试信息
|
||||
type: string
|
||||
success:
|
||||
description: 是否成功
|
||||
type: boolean
|
||||
required:
|
||||
- ros_node
|
||||
- return_info
|
||||
- success
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-print_status_summary:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: print_status_summary参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-test_connection:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: test_connection参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
export_status_json:
|
||||
@@ -145,29 +112,32 @@ neware_battery_test_system:
|
||||
goal:
|
||||
plate_num: plate_num
|
||||
goal_default:
|
||||
plate_num: 1
|
||||
plate_num: null
|
||||
handles: {}
|
||||
result:
|
||||
plate_data: plate_data
|
||||
return_info: return_info
|
||||
success: success
|
||||
schema:
|
||||
description: 获取指定盘(1或2)的电池状态信息
|
||||
description: 获取指定盘或所有盘的状态信息
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
plate_num:
|
||||
description: 盘号 (1 或 2)
|
||||
description: 盘号 (1 或 2),如果为null则返回所有盘的状态
|
||||
maximum: 2
|
||||
minimum: 1
|
||||
type: integer
|
||||
required:
|
||||
- plate_num
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
plate_data:
|
||||
description: 盘状态数据(单盘或所有盘)
|
||||
type: object
|
||||
return_info:
|
||||
description: 盘状态信息JSON格式
|
||||
description: 操作结果信息
|
||||
type: string
|
||||
success:
|
||||
description: 查询是否成功
|
||||
@@ -175,6 +145,7 @@ neware_battery_test_system:
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
- plate_data
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
@@ -219,7 +190,9 @@ neware_battery_test_system:
|
||||
goal_default:
|
||||
string: ''
|
||||
handles: {}
|
||||
result: {}
|
||||
result:
|
||||
return_info: return_info
|
||||
success: success
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
@@ -252,6 +225,56 @@ neware_battery_test_system:
|
||||
title: StrSingleInput
|
||||
type: object
|
||||
type: StrSingleInput
|
||||
submit_from_csv:
|
||||
feedback: {}
|
||||
goal:
|
||||
csv_path: string
|
||||
output_dir: string
|
||||
goal_default:
|
||||
csv_path: ''
|
||||
output_dir: .
|
||||
handles: {}
|
||||
result:
|
||||
return_info: return_info
|
||||
submitted_count: submitted_count
|
||||
success: success
|
||||
schema:
|
||||
description: 从CSV文件批量提交Neware测试任务
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
csv_path:
|
||||
description: 输入CSV文件的绝对路径
|
||||
type: string
|
||||
output_dir:
|
||||
description: 输出目录(用于存储XML和备份文件),默认当前目录
|
||||
type: string
|
||||
required:
|
||||
- csv_path
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
description: 执行结果详细信息
|
||||
type: string
|
||||
submitted_count:
|
||||
description: 成功提交的任务数量
|
||||
type: integer
|
||||
success:
|
||||
description: 是否成功
|
||||
type: boolean
|
||||
total_count:
|
||||
description: CSV文件中的总行数
|
||||
type: integer
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
test_connection_action:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
@@ -284,30 +307,135 @@ neware_battery_test_system:
|
||||
- goal
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.battery.neware_battery_test_system:NewareBatteryTestSystem
|
||||
upload_backup_to_oss:
|
||||
feedback: {}
|
||||
goal:
|
||||
backup_dir: backup_dir
|
||||
file_pattern: file_pattern
|
||||
oss_prefix: oss_prefix
|
||||
goal_default:
|
||||
backup_dir: null
|
||||
file_pattern: '*'
|
||||
oss_prefix: null
|
||||
handles:
|
||||
output:
|
||||
- data_key: uploaded_files
|
||||
data_source: executor
|
||||
data_type: array
|
||||
handler_key: uploaded_files
|
||||
io_type: sink
|
||||
label: Uploaded Files (with standard flow info)
|
||||
result:
|
||||
failed_files: failed_files
|
||||
return_info: return_info
|
||||
success: success
|
||||
total_count: total_count
|
||||
uploaded_count: uploaded_count
|
||||
schema:
|
||||
description: 上传备份文件到阿里云OSS
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
backup_dir:
|
||||
description: 备份目录路径(默认使用最近一次submit_from_csv的backup_dir)
|
||||
type: string
|
||||
file_pattern:
|
||||
default: '*'
|
||||
description: 文件通配符模式,例如 *.csv 或 Battery_*.nda
|
||||
type: string
|
||||
oss_prefix:
|
||||
description: OSS对象路径前缀(默认使用self.oss_prefix)
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
failed_files:
|
||||
description: 上传失败的文件名列表
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
return_info:
|
||||
description: 上传操作结果信息
|
||||
type: string
|
||||
success:
|
||||
description: 上传是否成功
|
||||
type: boolean
|
||||
total_count:
|
||||
description: 总文件数
|
||||
type: integer
|
||||
uploaded_count:
|
||||
description: 成功上传的文件数
|
||||
type: integer
|
||||
uploaded_files:
|
||||
description: 成功上传的文件详情列表
|
||||
items:
|
||||
properties:
|
||||
Battery_Code:
|
||||
description: 电池编码
|
||||
type: string
|
||||
Electrolyte_Code:
|
||||
description: 电解液编码
|
||||
type: string
|
||||
filename:
|
||||
description: 文件名
|
||||
type: string
|
||||
url:
|
||||
description: OSS下载链接
|
||||
type: string
|
||||
required:
|
||||
- filename
|
||||
- url
|
||||
- Battery_Code
|
||||
- Electrolyte_Code
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
- uploaded_count
|
||||
- total_count
|
||||
- failed_files
|
||||
- uploaded_files
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
|
||||
status_types:
|
||||
channel_status: dict
|
||||
connection_info: dict
|
||||
device_summary: dict
|
||||
plate_status: dict
|
||||
status: str
|
||||
total_channels: int
|
||||
type: python
|
||||
config_info: []
|
||||
description: 新威电池测试系统驱动,支持720个通道的电池测试状态监控和数据导出。通过TCP通信实现远程控制,包含完整的物料管理系统,支持2盘电池的状态映射和监控。
|
||||
description: 新威电池测试系统驱动,提供720个通道的电池测试状态监控、物料管理和CSV批量提交功能。支持TCP通信实现远程控制,包含完整的物料管理系统(2盘电池状态映射),以及从CSV文件批量提交测试任务的能力。
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
devtype:
|
||||
default: '27'
|
||||
type: string
|
||||
ip:
|
||||
default: 127.0.0.1
|
||||
type: string
|
||||
machine_id:
|
||||
default: 1
|
||||
type: integer
|
||||
oss_prefix:
|
||||
default: neware_backup
|
||||
description: OSS对象路径前缀
|
||||
type: string
|
||||
oss_upload_enabled:
|
||||
default: false
|
||||
description: 是否启用OSS上传功能
|
||||
type: boolean
|
||||
port:
|
||||
default: 502
|
||||
type: integer
|
||||
size_x:
|
||||
default: 500.0
|
||||
@@ -319,6 +447,7 @@ neware_battery_test_system:
|
||||
default: 2000.0
|
||||
type: number
|
||||
timeout:
|
||||
default: 20
|
||||
type: integer
|
||||
required: []
|
||||
type: object
|
||||
@@ -330,8 +459,6 @@ neware_battery_test_system:
|
||||
type: object
|
||||
device_summary:
|
||||
type: object
|
||||
plate_status:
|
||||
type: object
|
||||
status:
|
||||
type: string
|
||||
total_channels:
|
||||
@@ -341,7 +468,6 @@ neware_battery_test_system:
|
||||
- channel_status
|
||||
- connection_info
|
||||
- total_channels
|
||||
- plate_status
|
||||
- device_summary
|
||||
type: object
|
||||
version: 1.0.0
|
||||
|
||||
630
unilabos/registry/devices/post_process_station.yaml
Normal file
630
unilabos/registry/devices/post_process_station.yaml
Normal file
@@ -0,0 +1,630 @@
|
||||
post_process_station:
|
||||
category:
|
||||
- post_process_station
|
||||
class:
|
||||
action_value_mappings:
|
||||
disconnect:
|
||||
feedback: {}
|
||||
goal:
|
||||
command: {}
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
read_node:
|
||||
feedback:
|
||||
result: result
|
||||
goal:
|
||||
command: node_name
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
trigger_cleaning_action:
|
||||
feedback: {}
|
||||
goal:
|
||||
acetone_inner_wall_cleaning_count: acetone_inner_wall_cleaning_count
|
||||
acetone_inner_wall_cleaning_injection: acetone_inner_wall_cleaning_injection
|
||||
acetone_inner_wall_cleaning_waste_time: acetone_inner_wall_cleaning_waste_time
|
||||
acetone_outer_wall_cleaning_count: acetone_outer_wall_cleaning_count
|
||||
acetone_outer_wall_cleaning_injection: acetone_outer_wall_cleaning_injection
|
||||
acetone_outer_wall_cleaning_wait_time: acetone_outer_wall_cleaning_wait_time
|
||||
acetone_outer_wall_cleaning_waste_time: acetone_outer_wall_cleaning_waste_time
|
||||
acetone_pump_cleaning_suction_count: acetone_pump_cleaning_suction_count
|
||||
acetone_stirrer_cleaning_count: acetone_stirrer_cleaning_count
|
||||
acetone_stirrer_cleaning_injection: acetone_stirrer_cleaning_injection
|
||||
acetone_stirrer_cleaning_wait_time: acetone_stirrer_cleaning_wait_time
|
||||
acetone_stirrer_cleaning_waste_time: acetone_stirrer_cleaning_waste_time
|
||||
filtration_liquid_selection: filtration_liquid_selection
|
||||
injection_pump_forward_empty_suction_count: injection_pump_forward_empty_suction_count
|
||||
injection_pump_reverse_empty_suction_count: injection_pump_reverse_empty_suction_count
|
||||
nmp_inner_wall_cleaning_count: nmp_inner_wall_cleaning_count
|
||||
nmp_inner_wall_cleaning_injection: nmp_inner_wall_cleaning_injection
|
||||
nmp_inner_wall_cleaning_waste_time: nmp_inner_wall_cleaning_waste_time
|
||||
nmp_outer_wall_cleaning_count: nmp_outer_wall_cleaning_count
|
||||
nmp_outer_wall_cleaning_injection: nmp_outer_wall_cleaning_injection
|
||||
nmp_outer_wall_cleaning_wait_time: nmp_outer_wall_cleaning_wait_time
|
||||
nmp_outer_wall_cleaning_waste_time: nmp_outer_wall_cleaning_waste_time
|
||||
nmp_pump_cleaning_suction_count: nmp_pump_cleaning_suction_count
|
||||
nmp_stirrer_cleaning_count: nmp_stirrer_cleaning_count
|
||||
nmp_stirrer_cleaning_injection: nmp_stirrer_cleaning_injection
|
||||
nmp_stirrer_cleaning_wait_time: nmp_stirrer_cleaning_wait_time
|
||||
nmp_stirrer_cleaning_waste_time: nmp_stirrer_cleaning_waste_time
|
||||
pipe_blowing_time: pipe_blowing_time
|
||||
water_inner_wall_cleaning_count: water_inner_wall_cleaning_count
|
||||
water_inner_wall_cleaning_injection: water_inner_wall_cleaning_injection
|
||||
water_inner_wall_cleaning_waste_time: water_inner_wall_cleaning_waste_time
|
||||
water_outer_wall_cleaning_count: water_outer_wall_cleaning_count
|
||||
water_outer_wall_cleaning_injection: water_outer_wall_cleaning_injection
|
||||
water_outer_wall_cleaning_wait_time: water_outer_wall_cleaning_wait_time
|
||||
water_outer_wall_cleaning_waste_time: water_outer_wall_cleaning_waste_time
|
||||
water_pump_cleaning_suction_count: water_pump_cleaning_suction_count
|
||||
water_stirrer_cleaning_count: water_stirrer_cleaning_count
|
||||
water_stirrer_cleaning_injection: water_stirrer_cleaning_injection
|
||||
water_stirrer_cleaning_wait_time: water_stirrer_cleaning_wait_time
|
||||
water_stirrer_cleaning_waste_time: water_stirrer_cleaning_waste_time
|
||||
goal_default:
|
||||
acetone_inner_wall_cleaning_count: 0
|
||||
acetone_inner_wall_cleaning_injection: 0.0
|
||||
acetone_inner_wall_cleaning_waste_time: 0
|
||||
acetone_outer_wall_cleaning_count: 0
|
||||
acetone_outer_wall_cleaning_injection: 0.0
|
||||
acetone_outer_wall_cleaning_wait_time: 0
|
||||
acetone_outer_wall_cleaning_waste_time: 0
|
||||
acetone_pump_cleaning_suction_count: 0
|
||||
acetone_stirrer_cleaning_count: 0
|
||||
acetone_stirrer_cleaning_injection: 0.0
|
||||
acetone_stirrer_cleaning_wait_time: 0
|
||||
acetone_stirrer_cleaning_waste_time: 0
|
||||
filtration_liquid_selection: 0
|
||||
injection_pump_forward_empty_suction_count: 0
|
||||
injection_pump_reverse_empty_suction_count: 0
|
||||
nmp_inner_wall_cleaning_count: 0
|
||||
nmp_inner_wall_cleaning_injection: 0.0
|
||||
nmp_inner_wall_cleaning_waste_time: 0
|
||||
nmp_outer_wall_cleaning_count: 0
|
||||
nmp_outer_wall_cleaning_injection: 0.0
|
||||
nmp_outer_wall_cleaning_wait_time: 0
|
||||
nmp_outer_wall_cleaning_waste_time: 0
|
||||
nmp_pump_cleaning_suction_count: 0
|
||||
nmp_stirrer_cleaning_count: 0
|
||||
nmp_stirrer_cleaning_injection: 0.0
|
||||
nmp_stirrer_cleaning_wait_time: 0
|
||||
nmp_stirrer_cleaning_waste_time: 0
|
||||
pipe_blowing_time: 0
|
||||
water_inner_wall_cleaning_count: 0
|
||||
water_inner_wall_cleaning_injection: 0.0
|
||||
water_inner_wall_cleaning_waste_time: 0
|
||||
water_outer_wall_cleaning_count: 0
|
||||
water_outer_wall_cleaning_injection: 0.0
|
||||
water_outer_wall_cleaning_wait_time: 0
|
||||
water_outer_wall_cleaning_waste_time: 0
|
||||
water_pump_cleaning_suction_count: 0
|
||||
water_stirrer_cleaning_count: 0
|
||||
water_stirrer_cleaning_injection: 0.0
|
||||
water_stirrer_cleaning_wait_time: 0
|
||||
water_stirrer_cleaning_waste_time: 0
|
||||
handles: {}
|
||||
result:
|
||||
return_info: return_info
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: PostProcessTriggerClean_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
acetone_inner_wall_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
acetone_inner_wall_cleaning_injection:
|
||||
type: number
|
||||
acetone_inner_wall_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
acetone_outer_wall_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
acetone_outer_wall_cleaning_injection:
|
||||
type: number
|
||||
acetone_outer_wall_cleaning_wait_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
acetone_outer_wall_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
acetone_pump_cleaning_suction_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
acetone_stirrer_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
acetone_stirrer_cleaning_injection:
|
||||
type: number
|
||||
acetone_stirrer_cleaning_wait_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
acetone_stirrer_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
filtration_liquid_selection:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
injection_pump_forward_empty_suction_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
injection_pump_reverse_empty_suction_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_inner_wall_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_inner_wall_cleaning_injection:
|
||||
type: number
|
||||
nmp_inner_wall_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_outer_wall_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_outer_wall_cleaning_injection:
|
||||
type: number
|
||||
nmp_outer_wall_cleaning_wait_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_outer_wall_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_pump_cleaning_suction_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_stirrer_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_stirrer_cleaning_injection:
|
||||
type: number
|
||||
nmp_stirrer_cleaning_wait_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
nmp_stirrer_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
pipe_blowing_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_inner_wall_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_inner_wall_cleaning_injection:
|
||||
type: number
|
||||
water_inner_wall_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_outer_wall_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_outer_wall_cleaning_injection:
|
||||
type: number
|
||||
water_outer_wall_cleaning_wait_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_outer_wall_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_pump_cleaning_suction_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_stirrer_cleaning_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_stirrer_cleaning_injection:
|
||||
type: number
|
||||
water_stirrer_cleaning_wait_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
water_stirrer_cleaning_waste_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- nmp_outer_wall_cleaning_injection
|
||||
- nmp_outer_wall_cleaning_count
|
||||
- nmp_outer_wall_cleaning_wait_time
|
||||
- nmp_outer_wall_cleaning_waste_time
|
||||
- nmp_inner_wall_cleaning_injection
|
||||
- nmp_inner_wall_cleaning_count
|
||||
- nmp_pump_cleaning_suction_count
|
||||
- nmp_inner_wall_cleaning_waste_time
|
||||
- nmp_stirrer_cleaning_injection
|
||||
- nmp_stirrer_cleaning_count
|
||||
- nmp_stirrer_cleaning_wait_time
|
||||
- nmp_stirrer_cleaning_waste_time
|
||||
- water_outer_wall_cleaning_injection
|
||||
- water_outer_wall_cleaning_count
|
||||
- water_outer_wall_cleaning_wait_time
|
||||
- water_outer_wall_cleaning_waste_time
|
||||
- water_inner_wall_cleaning_injection
|
||||
- water_inner_wall_cleaning_count
|
||||
- water_pump_cleaning_suction_count
|
||||
- water_inner_wall_cleaning_waste_time
|
||||
- water_stirrer_cleaning_injection
|
||||
- water_stirrer_cleaning_count
|
||||
- water_stirrer_cleaning_wait_time
|
||||
- water_stirrer_cleaning_waste_time
|
||||
- acetone_outer_wall_cleaning_injection
|
||||
- acetone_outer_wall_cleaning_count
|
||||
- acetone_outer_wall_cleaning_wait_time
|
||||
- acetone_outer_wall_cleaning_waste_time
|
||||
- acetone_inner_wall_cleaning_injection
|
||||
- acetone_inner_wall_cleaning_count
|
||||
- acetone_pump_cleaning_suction_count
|
||||
- acetone_inner_wall_cleaning_waste_time
|
||||
- acetone_stirrer_cleaning_injection
|
||||
- acetone_stirrer_cleaning_count
|
||||
- acetone_stirrer_cleaning_wait_time
|
||||
- acetone_stirrer_cleaning_waste_time
|
||||
- pipe_blowing_time
|
||||
- injection_pump_forward_empty_suction_count
|
||||
- injection_pump_reverse_empty_suction_count
|
||||
- filtration_liquid_selection
|
||||
title: PostProcessTriggerClean_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: PostProcessTriggerClean_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: PostProcessTriggerClean
|
||||
type: object
|
||||
type: PostProcessTriggerClean
|
||||
trigger_grab_action:
|
||||
feedback: {}
|
||||
goal:
|
||||
raw_tank_number: raw_tank_number
|
||||
reaction_tank_number: reaction_tank_number
|
||||
goal_default:
|
||||
raw_tank_number: 0
|
||||
reaction_tank_number: 0
|
||||
handles: {}
|
||||
result:
|
||||
return_info: return_info
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: PostProcessGrab_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
raw_tank_number:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
reaction_tank_number:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
required:
|
||||
- reaction_tank_number
|
||||
- raw_tank_number
|
||||
title: PostProcessGrab_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: PostProcessGrab_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: PostProcessGrab
|
||||
type: object
|
||||
type: PostProcessGrab
|
||||
trigger_post_processing:
|
||||
feedback: {}
|
||||
goal:
|
||||
atomization_fast_speed: atomization_fast_speed
|
||||
atomization_pressure_kpa: atomization_pressure_kpa
|
||||
first_powder_mixing_tim: first_powder_mixing_tim
|
||||
first_powder_wash_count: first_powder_wash_count
|
||||
first_wash_water_amount: first_wash_water_amount
|
||||
initial_water_amount: initial_water_amount
|
||||
injection_pump_push_speed: injection_pump_push_speed
|
||||
injection_pump_suction_speed: injection_pump_suction_speed
|
||||
pre_filtration_mixing_time: pre_filtration_mixing_time
|
||||
raw_liquid_suction_count: raw_liquid_suction_count
|
||||
second_powder_mixing_time: second_powder_mixing_time
|
||||
second_powder_wash_count: second_powder_wash_count
|
||||
second_wash_water_amount: second_wash_water_amount
|
||||
wash_slow_speed: wash_slow_speed
|
||||
goal_default:
|
||||
atomization_fast_speed: 0.0
|
||||
atomization_pressure_kpa: 0
|
||||
first_powder_mixing_tim: 0
|
||||
first_powder_wash_count: 0
|
||||
first_wash_water_amount: 0.0
|
||||
initial_water_amount: 0.0
|
||||
injection_pump_push_speed: 0
|
||||
injection_pump_suction_speed: 0
|
||||
pre_filtration_mixing_time: 0
|
||||
raw_liquid_suction_count: 0
|
||||
second_powder_mixing_time: 0
|
||||
second_powder_wash_count: 0
|
||||
second_wash_water_amount: 0.0
|
||||
wash_slow_speed: 0.0
|
||||
handles: {}
|
||||
result:
|
||||
return_info: return_info
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: PostProcessTriggerPostPro_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
atomization_fast_speed:
|
||||
type: number
|
||||
atomization_pressure_kpa:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
first_powder_mixing_tim:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
first_powder_wash_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
first_wash_water_amount:
|
||||
type: number
|
||||
initial_water_amount:
|
||||
type: number
|
||||
injection_pump_push_speed:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
injection_pump_suction_speed:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
pre_filtration_mixing_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
raw_liquid_suction_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
second_powder_mixing_time:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
second_powder_wash_count:
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
type: integer
|
||||
second_wash_water_amount:
|
||||
type: number
|
||||
wash_slow_speed:
|
||||
type: number
|
||||
required:
|
||||
- atomization_fast_speed
|
||||
- wash_slow_speed
|
||||
- injection_pump_suction_speed
|
||||
- injection_pump_push_speed
|
||||
- raw_liquid_suction_count
|
||||
- first_wash_water_amount
|
||||
- second_wash_water_amount
|
||||
- first_powder_mixing_tim
|
||||
- second_powder_mixing_time
|
||||
- first_powder_wash_count
|
||||
- second_powder_wash_count
|
||||
- initial_water_amount
|
||||
- pre_filtration_mixing_time
|
||||
- atomization_pressure_kpa
|
||||
title: PostProcessTriggerPostPro_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
required:
|
||||
- return_info
|
||||
title: PostProcessTriggerPostPro_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: PostProcessTriggerPostPro
|
||||
type: object
|
||||
type: PostProcessTriggerPostPro
|
||||
write_node:
|
||||
feedback:
|
||||
result: result
|
||||
goal:
|
||||
command: json_input
|
||||
goal_default:
|
||||
command: ''
|
||||
handles: {}
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
title: SendCmd_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
required:
|
||||
- command
|
||||
title: SendCmd_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
return_info:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
required:
|
||||
- return_info
|
||||
- success
|
||||
title: SendCmd_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SendCmd
|
||||
type: object
|
||||
type: SendCmd
|
||||
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
|
||||
status_types:
|
||||
acetone_tank_empty_alarm: Bool
|
||||
atomization_fast_speed: Float64
|
||||
atomization_pressure_kpa: Int32
|
||||
cleaning_complete: Bool
|
||||
device_ready: Bool
|
||||
door_open_alarm: Bool
|
||||
grab_complete: Bool
|
||||
grab_trigger: Bool
|
||||
injection_pump_push_speed: Int32
|
||||
injection_pump_suction_speed: Int32
|
||||
nmp_tank_empty_alarm: Bool
|
||||
post_process_complete: Bool
|
||||
post_process_trigger: Bool
|
||||
raw_tank_number: Int32
|
||||
reaction_tank_number: Int32
|
||||
remote_mode: Bool
|
||||
wash_slow_speed: Float64
|
||||
waste_tank_full_alarm: Bool
|
||||
water_tank_empty_alarm: Bool
|
||||
type: python
|
||||
config_info: []
|
||||
description: 后处理站
|
||||
handles: []
|
||||
icon: post_process_station.webp
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
@@ -222,7 +222,7 @@ class Registry:
|
||||
abs_path = Path(path).absolute()
|
||||
resource_path = abs_path / "resources"
|
||||
files = list(resource_path.glob("*/*.yaml"))
|
||||
logger.debug(f"[UniLab Registry] resources: {resource_path.exists()}, total: {len(files)}")
|
||||
logger.trace(f"[UniLab Registry] load resources? {resource_path.exists()}, total: {len(files)}")
|
||||
current_resource_number = len(self.resource_type_registry) + 1
|
||||
for i, file in enumerate(files):
|
||||
with open(file, encoding="utf-8", mode="r") as f:
|
||||
@@ -237,6 +237,8 @@ class Registry:
|
||||
resource_info["category"] = [file.stem]
|
||||
elif file.stem not in resource_info["category"]:
|
||||
resource_info["category"].append(file.stem)
|
||||
elif not isinstance(resource_info.get("category"), list):
|
||||
resource_info["category"] = [resource_info["category"]]
|
||||
if "config_info" not in resource_info:
|
||||
resource_info["config_info"] = []
|
||||
if "icon" not in resource_info:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
POST_PROCESS_Raw_1BottleCarrier:
|
||||
category:
|
||||
- bottle_carriers
|
||||
class:
|
||||
module: unilabos.devices.workstation.post_process.bottle_carriers:POST_PROCESS_Raw_1BottleCarrier
|
||||
type: pylabrobot
|
||||
description: POST_PROCESS_Raw_1BottleCarrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
|
||||
POST_PROCESS_Reaction_1BottleCarrier:
|
||||
category:
|
||||
- bottle_carriers
|
||||
class:
|
||||
module: unilabos.devices.workstation.post_process.bottle_carriers:POST_PROCESS_Reaction_1BottleCarrier
|
||||
type: pylabrobot
|
||||
description: POST_PROCESS_Reaction_1BottleCarrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
11
unilabos/registry/resources/post_process/bottles.yaml
Normal file
11
unilabos/registry/resources/post_process/bottles.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
POST_PROCESS_PolymerStation_Reagent_Bottle:
|
||||
category:
|
||||
- bottles
|
||||
class:
|
||||
module: unilabos.devices.workstation.post_process.bottles:POST_PROCESS_PolymerStation_Reagent_Bottle
|
||||
type: pylabrobot
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
|
||||
12
unilabos/registry/resources/post_process/deck.yaml
Normal file
12
unilabos/registry/resources/post_process/deck.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
post_process_deck:
|
||||
category:
|
||||
- post_process_deck
|
||||
class:
|
||||
module: unilabos.devices.workstation.post_process.decks:post_process_deck
|
||||
type: pylabrobot
|
||||
description: post_process_deck
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
108
unilabos/registry/resources/prcxi/plate_adapters.yaml
Normal file
108
unilabos/registry/resources/prcxi/plate_adapters.yaml
Normal file
@@ -0,0 +1,108 @@
|
||||
PRCXI_30mm_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_30mm_Adapter
|
||||
type: pylabrobot
|
||||
description: '30mm适配器 (Code: ZX-58-30)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Adapter
|
||||
type: pylabrobot
|
||||
description: '适配器 (Code: Fhh478)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_Deep10_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Deep10_Adapter
|
||||
type: pylabrobot
|
||||
description: '10ul专用深孔板适配器 (Code: ZX-002-10)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_Deep300_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Deep300_Adapter
|
||||
type: pylabrobot
|
||||
description: '300ul深孔板适配器 (Code: ZX-002-300)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_PCR_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Adapter
|
||||
type: pylabrobot
|
||||
description: '全裙边 PCR适配器 (Code: ZX-58-0001)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_Reservoir_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Reservoir_Adapter
|
||||
type: pylabrobot
|
||||
description: '储液槽 适配器 (Code: ZX-ADP-001)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_Tip10_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip10_Adapter
|
||||
type: pylabrobot
|
||||
description: '吸头10ul 适配器 (Code: ZX-58-10)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_Tip1250_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip1250_Adapter
|
||||
type: pylabrobot
|
||||
description: 'Tip头适配器 1250uL (Code: ZX-58-1250)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_Tip300_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip300_Adapter
|
||||
type: pylabrobot
|
||||
description: 'ZHONGXI 适配器 300uL (Code: ZX-58-300)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
@@ -1,10 +1,130 @@
|
||||
prcxi_96_wellplate_360ul_flat:
|
||||
category:
|
||||
- plates
|
||||
PRCXI_48_DeepWell:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_res:prcxi_96_wellplate_360ul_flat
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_48_DeepWell
|
||||
type: pylabrobot
|
||||
description: prcxi_96_wellplate_360ul_flat
|
||||
description: '48孔深孔板 (Code: 22)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_96_DeepWell:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_96_DeepWell
|
||||
type: pylabrobot
|
||||
description: '96深孔板 (Code: q2)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_AGenBio_4_troughplate:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_AGenBio_4_troughplate
|
||||
type: pylabrobot
|
||||
description: '4道储液槽 (Code: sdfrth654)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_BioER_96_wellplate:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_BioER_96_wellplate
|
||||
type: pylabrobot
|
||||
description: '2.2ml 深孔板 (Code: ZX-019-2.2)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_BioRad_384_wellplate:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_BioRad_384_wellplate
|
||||
type: pylabrobot
|
||||
description: '384板 (Code: q3)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_CellTreat_96_wellplate:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_CellTreat_96_wellplate
|
||||
type: pylabrobot
|
||||
description: '细菌培养皿 (Code: ZX-78-096)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_PCR_Plate_200uL_nonskirted:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_nonskirted
|
||||
type: pylabrobot
|
||||
description: '0.2ml PCR 板 (Code: ZX-023-0.2)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_PCR_Plate_200uL_semiskirted:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_semiskirted
|
||||
type: pylabrobot
|
||||
description: '0.2ml PCR 板 (Code: ZX-023-0.2)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_PCR_Plate_200uL_skirted:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_skirted
|
||||
type: pylabrobot
|
||||
description: '0.2ml PCR 板 (Code: ZX-023-0.2)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_nest_12_troughplate:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_nest_12_troughplate
|
||||
type: pylabrobot
|
||||
description: '12道储液槽 (Code: 12道储液槽)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_nest_1_troughplate:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_nest_1_troughplate
|
||||
type: pylabrobot
|
||||
description: '储液槽 (Code: ZX-58-10000)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
|
||||
@@ -1,23 +1,70 @@
|
||||
prcxi_opentrons_96_tiprack_10ul:
|
||||
category:
|
||||
- tip_racks
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_res:prcxi_opentrons_96_tiprack_10ul
|
||||
type: pylabrobot
|
||||
description: prcxi_opentrons_96_tiprack_10ul
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
tip_adaptor_1250ul_2:
|
||||
category:
|
||||
PRCXI_1000uL_Tips:
|
||||
category:
|
||||
- prcxi
|
||||
- tip_racks
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_materials:tip_adaptor_1250ul
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_1000uL_Tips
|
||||
type: pylabrobot
|
||||
description: Tip头适配器 1250uL
|
||||
description: '1000μL Tip头 (Code: ZX-001-1000)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_10uL_Tips:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_10uL_Tips
|
||||
type: pylabrobot
|
||||
description: '10μL Tip头 (Code: ZX-001-10)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_10ul_eTips:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_10ul_eTips
|
||||
type: pylabrobot
|
||||
description: '10μL加长 Tip头 (Code: ZX-001-10+)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_1250uL_Tips:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_1250uL_Tips
|
||||
type: pylabrobot
|
||||
description: '1250μL Tip头 (Code: ZX-001-1250)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_200uL_Tips:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_200uL_Tips
|
||||
type: pylabrobot
|
||||
description: '200μL Tip头 (Code: ZX-001-200)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
PRCXI_300ul_Tips:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_300ul_Tips
|
||||
type: pylabrobot
|
||||
description: '300μL Tip头 (Code: ZX-001-300)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
prcxi_trash:
|
||||
category:
|
||||
- trash
|
||||
PRCXI_trash:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_res:prcxi_trash
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_trash
|
||||
type: pylabrobot
|
||||
description: prcxi_trash
|
||||
description: '废弃槽 (Code: q1)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
|
||||
12
unilabos/registry/resources/prcxi/tube_racks.yaml
Normal file
12
unilabos/registry/resources/prcxi/tube_racks.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
PRCXI_EP_Adapter:
|
||||
category:
|
||||
- prcxi
|
||||
class:
|
||||
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_EP_Adapter
|
||||
type: pylabrobot
|
||||
description: 'ep适配器 (Code: 1)'
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
@@ -42,7 +42,7 @@ def canonicalize_nodes_data(
|
||||
Returns:
|
||||
ResourceTreeSet: 标准化后的资源树集合
|
||||
"""
|
||||
print_status(f"{len(nodes)} Resources loaded:", "info")
|
||||
print_status(f"{len(nodes)} Resources loaded", "info")
|
||||
|
||||
# 第一步:基本预处理(处理graphml的label字段)
|
||||
outer_host_node_id = None
|
||||
@@ -284,10 +284,18 @@ def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]
|
||||
edge["sourceHandle"] = port[source]
|
||||
elif "source_port" in edge:
|
||||
edge["sourceHandle"] = edge.pop("source_port")
|
||||
else:
|
||||
typ = edge.get("type")
|
||||
if typ == "communication":
|
||||
continue
|
||||
if target in port:
|
||||
edge["targetHandle"] = port[target]
|
||||
elif "target_port" in edge:
|
||||
edge["targetHandle"] = edge.pop("target_port")
|
||||
else:
|
||||
typ = edge.get("type")
|
||||
if typ == "communication":
|
||||
continue
|
||||
edge["id"] = f"reactflow__edge-{source}-{edge['sourceHandle']}-{target}-{edge['targetHandle']}"
|
||||
for key in ["source_port", "target_port"]:
|
||||
if key in edge:
|
||||
|
||||
@@ -4,7 +4,11 @@ def register():
|
||||
# noinspection PyUnresolvedReferences
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Deck
|
||||
# noinspection PyUnresolvedReferences
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Plate
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300PlateAdapter
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TipRack
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Trash
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TubeRack
|
||||
# noinspection PyUnresolvedReferences
|
||||
from unilabos.devices.workstation.workstation_base import WorkStationContainer
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ class ResourceDict(BaseModel):
|
||||
klass: str = Field(alias="class", description="Resource class name")
|
||||
pose: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition)
|
||||
config: Dict[str, Any] = Field(description="Resource configuration")
|
||||
data: Dict[str, Any] = Field(description="Resource data")
|
||||
extra: Dict[str, Any] = Field(description="Extra data")
|
||||
data: Dict[str, Any] = Field(description="Resource data, eg: container liquid data")
|
||||
extra: Dict[str, Any] = Field(description="Extra data, eg: slot index")
|
||||
|
||||
@field_serializer("parent_uuid")
|
||||
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "device",
|
||||
"class": "syringepump.runze",
|
||||
"class": "syringe_pump_with_valve.runze.SY03B-T08",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
@@ -93,7 +93,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 430.4087301587302,
|
||||
"y": 428,
|
||||
@@ -117,7 +117,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 295.36944444444447,
|
||||
"y": 428,
|
||||
@@ -141,7 +141,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 165.36944444444444,
|
||||
"y": 428,
|
||||
@@ -165,7 +165,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 165.36944444444444,
|
||||
"y": 428,
|
||||
@@ -189,7 +189,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 35,
|
||||
"y": 428,
|
||||
@@ -213,7 +213,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 698.1111111111111,
|
||||
"y": 428,
|
||||
@@ -255,7 +255,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "device",
|
||||
"class": "syringepump.runze",
|
||||
"class": "syringe_pump_with_valve.runze.SY03B-T08",
|
||||
"position": {
|
||||
"x": 1195.611507936508,
|
||||
"y": 686,
|
||||
@@ -279,7 +279,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 1587.703373015873,
|
||||
"y": 1172.5,
|
||||
@@ -299,7 +299,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "device",
|
||||
"class": "separator_controller",
|
||||
"class": "separator.homemade",
|
||||
"position": {
|
||||
"x": 1624.4027777777778,
|
||||
"y": 665.5,
|
||||
@@ -320,7 +320,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 1614.404365079365,
|
||||
"y": 948,
|
||||
@@ -340,7 +340,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 1915.7035714285714,
|
||||
"y": 665.5,
|
||||
@@ -360,7 +360,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 1785.7035714285714,
|
||||
"y": 665.5,
|
||||
@@ -384,7 +384,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 2054.0650793650793,
|
||||
"y": 665.5,
|
||||
@@ -408,7 +408,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "device",
|
||||
"class": "syringepump.runze",
|
||||
"class": "syringe_pump_with_valve.runze.SY03B-T08",
|
||||
"position": {
|
||||
"x": 1630.6527777777778,
|
||||
"y": 448.5,
|
||||
@@ -432,7 +432,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "device",
|
||||
"class": "rotavap",
|
||||
"class": "rotavap.one",
|
||||
"position": {
|
||||
"x": 1339.7031746031746,
|
||||
"y": 968.5,
|
||||
@@ -453,7 +453,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 1339.7031746031746,
|
||||
"y": 1152,
|
||||
@@ -473,7 +473,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 909.722619047619,
|
||||
"y": 948,
|
||||
@@ -493,7 +493,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 867.972619047619,
|
||||
"y": 1152,
|
||||
@@ -513,7 +513,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 742.722619047619,
|
||||
"y": 948,
|
||||
@@ -533,7 +533,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 1206.722619047619,
|
||||
"y": 948,
|
||||
@@ -553,7 +553,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"class": "container",
|
||||
"position": {
|
||||
"x": 1148.222619047619,
|
||||
"y": 1152,
|
||||
@@ -573,7 +573,7 @@
|
||||
"children": [],
|
||||
"parent": "YugongStation",
|
||||
"type": "device",
|
||||
"class": "syringepump.runze",
|
||||
"class": "syringe_pump_with_valve.runze.SY03B-T08",
|
||||
"position": {
|
||||
"x": 1469.7031746031746,
|
||||
"y": 968.5,
|
||||
|
||||
34
unilabos/test/experiments/chinwe.json
Normal file
34
unilabos/test/experiments/chinwe.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "ChinWeStation",
|
||||
"name": "分液工作站",
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "separator.chinwe",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"port": "192.168.31.13:8899",
|
||||
"baudrate": 9600,
|
||||
"pump_ids": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"motor_ids": [
|
||||
4,
|
||||
5
|
||||
],
|
||||
"sensor_id": 6,
|
||||
"sensor_threshold": 300
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
@@ -178,7 +178,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300TipRack",
|
||||
"size_x": 50,
|
||||
"size_y": 40,
|
||||
"size_z": 30,
|
||||
@@ -4248,7 +4248,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 40,
|
||||
"size_z": 30,
|
||||
@@ -9415,7 +9415,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 40,
|
||||
"size_z": 30,
|
||||
@@ -13389,7 +13389,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 40,
|
||||
"size_z": 30,
|
||||
@@ -17363,7 +17363,7 @@
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Container",
|
||||
"type": "PRCXI9300Plate",
|
||||
"size_x": 50,
|
||||
"size_y": 40,
|
||||
"size_z": 30,
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "3a1c62c4-c3d2-b803-b72d-7f1153ffef3b",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00050",
|
||||
"barCode": "",
|
||||
"name": "NMP",
|
||||
"quantity": 287.16699029126215,
|
||||
"lockQuantity": 285.16699029126215,
|
||||
"unit": "毫升",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14198c-c2d0-efce-0939-69ca5a7dfd39",
|
||||
"whid": "3a14198c-c2cc-0290-e086-44a428fba248",
|
||||
"whName": "试剂堆栈",
|
||||
"code": "0001-0008",
|
||||
"x": 2,
|
||||
"y": 4,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1cdefe-0e03-1bc1-1296-dae1905c4108",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00052",
|
||||
"barCode": "",
|
||||
"name": "NMP",
|
||||
"quantity": 386.8990291262136,
|
||||
"lockQuantity": 45.89902912621359,
|
||||
"unit": "毫升",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14198c-c2d0-f3e7-871a-e470d144296f",
|
||||
"whid": "3a14198c-c2cc-0290-e086-44a428fba248",
|
||||
"whName": "试剂堆栈",
|
||||
"code": "0001-0005",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1cdefe-0e03-68a4-bcb3-02fc6ba72d1b",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00053",
|
||||
"barCode": "",
|
||||
"name": "NMP",
|
||||
"quantity": 400.0,
|
||||
"lockQuantity": 0.0,
|
||||
"unit": "",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
|
||||
"whid": "3a14198c-c2cc-0290-e086-44a428fba248",
|
||||
"whName": "试剂堆栈",
|
||||
"code": "0001-0006",
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1cdefe-d5e0-d850-5439-4499f20f07fe",
|
||||
"typeName": "分装板",
|
||||
"code": "0007-00185",
|
||||
"barCode": "",
|
||||
"name": "1010",
|
||||
"quantity": 1.0,
|
||||
"lockQuantity": 2.0,
|
||||
"unit": "块",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14198e-6929-46fe-841e-03dd753f1e4a",
|
||||
"whid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0",
|
||||
"whName": "粉末堆栈",
|
||||
"code": "0002-0009",
|
||||
"x": 3,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"id": "3a1cdefe-d5e0-28a4-f5d0-f7e2436c575f",
|
||||
"detailMaterialId": "3a1cdefe-d5e0-94ae-f770-27847e73ad38",
|
||||
"code": null,
|
||||
"name": "90%分装小瓶",
|
||||
"quantity": "1",
|
||||
"lockQuantity": "1",
|
||||
"unit": "个",
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cdefe-d5e0-3ed6-3607-133df89baf5b",
|
||||
"detailMaterialId": "3a1cdefe-d5e0-f2fa-66bf-94c565d852fb",
|
||||
"code": null,
|
||||
"name": "10%分装小瓶",
|
||||
"quantity": "1",
|
||||
"lockQuantity": "1",
|
||||
"unit": "个",
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cdefe-d5e0-72b6-e015-be7b93cf09eb",
|
||||
"detailMaterialId": "3a1cdefe-d5e0-81cf-7dad-2e51cab9ffd6",
|
||||
"code": null,
|
||||
"name": "90%分装小瓶",
|
||||
"quantity": "1",
|
||||
"lockQuantity": "1",
|
||||
"unit": "个",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cdefe-d5e0-81d3-ad30-48134afc9ce7",
|
||||
"detailMaterialId": "3a1cdefe-d5e0-3fa1-cc72-fda6276ae38d",
|
||||
"code": null,
|
||||
"name": "10%分装小瓶",
|
||||
"quantity": "1",
|
||||
"lockQuantity": "1",
|
||||
"unit": "个",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cdefe-d5e0-dbdf-d966-9a8926fe1e06",
|
||||
"detailMaterialId": "3a1cdefe-d5e0-c632-c7da-02d385b18628",
|
||||
"code": null,
|
||||
"name": "10%分装小瓶",
|
||||
"quantity": "1",
|
||||
"lockQuantity": "1",
|
||||
"unit": "个",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cdefe-d5e0-f099-b260-e3089a2d08c3",
|
||||
"detailMaterialId": "3a1cdefe-d5e0-561f-73b6-f8501f814dbb",
|
||||
"code": null,
|
||||
"name": "90%分装小瓶",
|
||||
"quantity": "1",
|
||||
"lockQuantity": "1",
|
||||
"unit": "个",
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,216 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "3a1cde21-a4f4-4f95-6221-eaafc2ae6a8d",
|
||||
"typeName": "样品瓶",
|
||||
"code": "0002-00407",
|
||||
"barCode": "",
|
||||
"name": "ODA",
|
||||
"quantity": 25.0,
|
||||
"lockQuantity": 2.0,
|
||||
"unit": "克",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1cde21-a4f4-7887-9258-e8f8ab7c8a7a",
|
||||
"typeName": "样品板",
|
||||
"code": "0008-00160",
|
||||
"barCode": "",
|
||||
"name": "1010sample",
|
||||
"quantity": 1.0,
|
||||
"lockQuantity": 27.69187,
|
||||
"unit": "块",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14198e-6929-4379-affa-9a2935c17f99",
|
||||
"whid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0",
|
||||
"whName": "粉末堆栈",
|
||||
"code": "0002-0002",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"id": "3a1cde21-a4f4-0339-f2b6-8e680ad7e8c7",
|
||||
"detailMaterialId": "3a1cde21-a4f4-ab37-f7a2-ecc3bc083e7c",
|
||||
"code": null,
|
||||
"name": "MPDA",
|
||||
"quantity": "10.505",
|
||||
"lockQuantity": "-0.0174",
|
||||
"unit": "克",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cde21-a4f4-a21a-23cf-bb7857b41947",
|
||||
"detailMaterialId": "3a1cde21-a4f4-99c7-55e7-c80c7320e300",
|
||||
"code": null,
|
||||
"name": "ODA",
|
||||
"quantity": "1.795",
|
||||
"lockQuantity": "2.0093",
|
||||
"unit": "克",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cde21-a4f4-af1b-ba0b-2874836800e9",
|
||||
"detailMaterialId": "3a1cde21-a4f4-4f95-6221-eaafc2ae6a8d",
|
||||
"code": null,
|
||||
"name": "ODA",
|
||||
"quantity": "25",
|
||||
"lockQuantity": "2",
|
||||
"unit": "克",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "3a1cde21-a4f4-99c7-55e7-c80c7320e300",
|
||||
"typeName": "样品瓶",
|
||||
"code": "0002-00406",
|
||||
"barCode": "",
|
||||
"name": "ODA",
|
||||
"quantity": 1.795,
|
||||
"lockQuantity": 2.00927,
|
||||
"unit": "克",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1cde21-a4f4-ab37-f7a2-ecc3bc083e7c",
|
||||
"typeName": "样品瓶",
|
||||
"code": "0002-00408",
|
||||
"barCode": "",
|
||||
"name": "MPDA",
|
||||
"quantity": 10.505,
|
||||
"lockQuantity": -0.0174,
|
||||
"unit": "克",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1cdeff-c92a-08f6-c822-732ab734154c",
|
||||
"typeName": "样品板",
|
||||
"code": "0008-00161",
|
||||
"barCode": "",
|
||||
"name": "1010sample2",
|
||||
"quantity": 1.0,
|
||||
"lockQuantity": 3.0,
|
||||
"unit": "块",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14198e-6929-31f0-8a22-0f98f72260df",
|
||||
"whid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0",
|
||||
"whName": "粉末堆栈",
|
||||
"code": "0002-0001",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"id": "3a1cdeff-c92b-3ace-9623-0bcdef6fa07d",
|
||||
"detailMaterialId": "3a1cdeff-c92b-d084-2a96-5d62746d9321",
|
||||
"code": null,
|
||||
"name": "BTDA1",
|
||||
"quantity": "0.362",
|
||||
"lockQuantity": "14.494",
|
||||
"unit": "克",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cdeff-c92b-856e-f481-792b91b6dbde",
|
||||
"detailMaterialId": "3a1cdeff-c92b-30f2-f907-8f5e2fe0586b",
|
||||
"code": null,
|
||||
"name": "BTDA3",
|
||||
"quantity": "1.935",
|
||||
"lockQuantity": "13.067",
|
||||
"unit": "克",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1cdeff-c92b-d144-c5e5-ab9d94e21187",
|
||||
"detailMaterialId": "3a1cdeff-c92b-519f-a70f-0bb71af537a7",
|
||||
"code": null,
|
||||
"name": "BTDA2",
|
||||
"quantity": "1.903",
|
||||
"lockQuantity": "13.035",
|
||||
"unit": "克",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "3a1cdeff-c92b-30f2-f907-8f5e2fe0586b",
|
||||
"typeName": "样品瓶",
|
||||
"code": "0002-00411",
|
||||
"barCode": "",
|
||||
"name": "BTDA3",
|
||||
"quantity": 1.935,
|
||||
"lockQuantity": 13.067,
|
||||
"unit": "克",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1cdeff-c92b-519f-a70f-0bb71af537a7",
|
||||
"typeName": "样品瓶",
|
||||
"code": "0002-00410",
|
||||
"barCode": "",
|
||||
"name": "BTDA2",
|
||||
"quantity": 1.903,
|
||||
"lockQuantity": 13.035,
|
||||
"unit": "克",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1cdeff-c92b-d084-2a96-5d62746d9321",
|
||||
"typeName": "样品瓶",
|
||||
"code": "0002-00409",
|
||||
"barCode": "",
|
||||
"name": "BTDA1",
|
||||
"quantity": 0.362,
|
||||
"lockQuantity": 14.494,
|
||||
"unit": "克",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [],
|
||||
"detail": []
|
||||
}
|
||||
]
|
||||
@@ -1,193 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "3a1c67a9-aed7-b94d-9e24-bfdf10c8baa9",
|
||||
"typeName": "烧杯",
|
||||
"code": "0006-00160",
|
||||
"barCode": "",
|
||||
"name": "ODA",
|
||||
"quantity": 120000.00000000000000000000000,
|
||||
"lockQuantity": 695374.00000000000000000000000,
|
||||
"unit": "微升",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0001",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-1ade-5fe1-cc04b24b171c",
|
||||
"typeName": "烧杯",
|
||||
"code": "0006-00161",
|
||||
"barCode": "",
|
||||
"name": "MPDA",
|
||||
"quantity": 120000.00000000000000000000000,
|
||||
"lockQuantity": 681618.00000000000000000000000,
|
||||
"unit": "",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-4bc5-8836-517b75473f5f",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0002",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-2864-6783-2cee4e701ba6",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00041",
|
||||
"barCode": "",
|
||||
"name": "NMP",
|
||||
"quantity": 300000.00000000000000000000000,
|
||||
"lockQuantity": 380000.00000000000000000000000,
|
||||
"unit": "微升",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5",
|
||||
"whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||
"whName": "站内试剂存放堆栈",
|
||||
"code": "0003-0001",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-32c7-5809-3ba1b8db1aa1",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00042",
|
||||
"barCode": "",
|
||||
"name": "PGME",
|
||||
"quantity": 300000.00000000000000000000000,
|
||||
"lockQuantity": 337892.00000000000000000000000,
|
||||
"unit": "",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa3b-9fab-ca72-febc-b7c304476c78",
|
||||
"whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||
"whName": "站内试剂存放堆栈",
|
||||
"code": "0003-0002",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-d748-725e-97a2e549f085",
|
||||
"typeName": "样品板",
|
||||
"code": "0001-00004",
|
||||
"barCode": "",
|
||||
"name": "0917",
|
||||
"quantity": 1.0000000000000000000000000000,
|
||||
"lockQuantity": 4.0000000000000000000000000000,
|
||||
"unit": "块",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-f49c-6b66-b27f185a3b32",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0009",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"id": "3a1c68c8-0574-69a1-9858-4637e0193451",
|
||||
"detailMaterialId": "3a1c68c8-0574-3630-bd42-bbf3623c5208",
|
||||
"code": null,
|
||||
"name": "SIDA",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-8d51-3191-a31f5be421e5",
|
||||
"detailMaterialId": "3a1c68c8-0574-3b20-9ad7-90755f123d53",
|
||||
"code": null,
|
||||
"name": "BTDA-2",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-da80-735b-53ae2197a360",
|
||||
"detailMaterialId": "3a1c68c8-0574-f2e4-33b3-90d813567939",
|
||||
"code": null,
|
||||
"name": "BTDA-DD",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "28",
|
||||
"unit": "微升",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-e717-1b1b-99891f875455",
|
||||
"detailMaterialId": "3a1c68c8-0574-a0ef-e636-68cdc98960e2",
|
||||
"code": null,
|
||||
"name": "BTDA-3",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-e9bd-6cca-5e261b4f89cb",
|
||||
"detailMaterialId": "3a1c68c8-0574-9d11-5115-283e8e5510b1",
|
||||
"code": null,
|
||||
"name": "BTDA-1",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,48 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier
|
||||
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
|
||||
|
||||
|
||||
def test_bottle_carrier() -> "BottleCarrier":
|
||||
print("创建载架...")
|
||||
|
||||
# 创建6瓶载架
|
||||
bottle_carrier = BIOYOND_Electrolyte_6VialCarrier("powder_carrier_01")
|
||||
print(f"6瓶载架: {bottle_carrier.name}, 位置数: {len(bottle_carrier.sites)}")
|
||||
|
||||
# 创建1烧杯载架
|
||||
beaker_carrier = BIOYOND_Electrolyte_1BottleCarrier("solution_carrier_01")
|
||||
print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}")
|
||||
|
||||
# 创建瓶子和烧杯
|
||||
powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01")
|
||||
solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01")
|
||||
reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01")
|
||||
|
||||
print(f"\n创建的物料:")
|
||||
print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL")
|
||||
print(f"溶液烧杯: {solution_beaker.name} - {solution_beaker.diameter}mm x {solution_beaker.height}mm, {solution_beaker.max_volume}μL")
|
||||
print(f"试剂瓶: {reagent_bottle.name} - {reagent_bottle.diameter}mm x {reagent_bottle.height}mm, {reagent_bottle.max_volume}μL")
|
||||
|
||||
# 测试放置容器
|
||||
print(f"\n测试放置容器...")
|
||||
|
||||
# 通过载架的索引操作来放置容器
|
||||
# bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置
|
||||
print(f"粉末瓶已放置到6瓶载架的位置 0")
|
||||
|
||||
# beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置
|
||||
print(f"溶液烧杯已放置到1烧杯载架的位置 0")
|
||||
|
||||
# 验证放置结果
|
||||
print(f"\n验证放置结果:")
|
||||
bottle_at_0 = bottle_carrier[0].resource
|
||||
beaker_at_0 = beaker_carrier[0].resource
|
||||
|
||||
if bottle_at_0:
|
||||
print(f"位置 0 的瓶子: {bottle_at_0.name}")
|
||||
if beaker_at_0:
|
||||
print(f"位置 0 的烧杯: {beaker_at_0.name}")
|
||||
|
||||
print("\n载架设置完成!")
|
||||
@@ -1,66 +0,0 @@
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
|
||||
from pylabrobot.resources import Resource as ResourcePLR
|
||||
from unilabos.resources.graphio import resource_bioyond_to_plr
|
||||
from unilabos.registry.registry import lab_registry
|
||||
|
||||
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
||||
|
||||
lab_registry.setup()
|
||||
|
||||
|
||||
type_mapping = {
|
||||
"烧杯": ("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"),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bioyond_materials_reaction() -> list[dict]:
|
||||
print("加载 BioYond 物料数据...")
|
||||
print(os.getcwd())
|
||||
with open("bioyond_materials_reaction.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bioyond_materials_liquidhandling_1() -> list[dict]:
|
||||
print("加载 BioYond 物料数据...")
|
||||
print(os.getcwd())
|
||||
with open("bioyond_materials_liquidhandling_1.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bioyond_materials_liquidhandling_2() -> list[dict]:
|
||||
print("加载 BioYond 物料数据...")
|
||||
print(os.getcwd())
|
||||
with open("bioyond_materials_liquidhandling_2.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("materials_fixture", [
|
||||
"bioyond_materials_reaction",
|
||||
"bioyond_materials_liquidhandling_1",
|
||||
])
|
||||
def test_bioyond_to_plr(materials_fixture, request) -> list[dict]:
|
||||
materials = request.getfixturevalue(materials_fixture)
|
||||
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
|
||||
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
|
||||
print(deck.summary())
|
||||
print([resource.serialize() for resource in output])
|
||||
print([resource.serialize_all_state() for resource in output])
|
||||
json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试修改后的 get_child_identifier 函数
|
||||
"""
|
||||
|
||||
from unilabos.resources.itemized_carrier import ItemizedCarrier, Bottle
|
||||
from pylabrobot.resources.coordinate import Coordinate
|
||||
|
||||
def test_get_child_identifier_with_indices():
|
||||
"""测试返回x,y,z索引的 get_child_identifier 函数"""
|
||||
|
||||
# 创建一些测试瓶子
|
||||
bottle1 = Bottle("bottle1", diameter=25.0, height=50.0, max_volume=15.0)
|
||||
bottle1.location = Coordinate(10, 20, 5)
|
||||
|
||||
bottle2 = Bottle("bottle2", diameter=25.0, height=50.0, max_volume=15.0)
|
||||
bottle2.location = Coordinate(50, 20, 5)
|
||||
|
||||
bottle3 = Bottle("bottle3", diameter=25.0, height=50.0, max_volume=15.0)
|
||||
bottle3.location = Coordinate(90, 20, 5)
|
||||
|
||||
# 创建载架,指定维度
|
||||
sites = {
|
||||
"A1": bottle1,
|
||||
"A2": bottle2,
|
||||
"A3": bottle3,
|
||||
"B1": None, # 空位
|
||||
"B2": None,
|
||||
"B3": None
|
||||
}
|
||||
|
||||
carrier = ItemizedCarrier(
|
||||
name="test_carrier",
|
||||
size_x=150,
|
||||
size_y=100,
|
||||
size_z=30,
|
||||
num_items_x=3, # 3列
|
||||
num_items_y=2, # 2行
|
||||
num_items_z=1, # 1层
|
||||
sites=sites
|
||||
)
|
||||
|
||||
print("测试载架维度:")
|
||||
print(f"num_items_x: {carrier.num_items_x}")
|
||||
print(f"num_items_y: {carrier.num_items_y}")
|
||||
print(f"num_items_z: {carrier.num_items_z}")
|
||||
print()
|
||||
|
||||
# 测试获取bottle1的标识符信息 (A1 = idx:0, x:0, y:0, z:0)
|
||||
result1 = carrier.get_child_identifier(bottle1)
|
||||
print("测试bottle1 (A1):")
|
||||
print(f" identifier: {result1['identifier']}")
|
||||
print(f" idx: {result1['idx']}")
|
||||
print(f" x index: {result1['x']}")
|
||||
print(f" y index: {result1['y']}")
|
||||
print(f" z index: {result1['z']}")
|
||||
|
||||
# Assert 验证 bottle1 (A1) 的结果
|
||||
assert result1['identifier'] == 'A1', f"Expected identifier 'A1', got '{result1['identifier']}'"
|
||||
assert result1['idx'] == 0, f"Expected idx 0, got {result1['idx']}"
|
||||
assert result1['x'] == 0, f"Expected x index 0, got {result1['x']}"
|
||||
assert result1['y'] == 0, f"Expected y index 0, got {result1['y']}"
|
||||
assert result1['z'] == 0, f"Expected z index 0, got {result1['z']}"
|
||||
print(" ✓ bottle1 (A1) 测试通过")
|
||||
print()
|
||||
|
||||
# 测试获取bottle2的标识符信息 (A2 = idx:1, x:1, y:0, z:0)
|
||||
result2 = carrier.get_child_identifier(bottle2)
|
||||
print("测试bottle2 (A2):")
|
||||
print(f" identifier: {result2['identifier']}")
|
||||
print(f" idx: {result2['idx']}")
|
||||
print(f" x index: {result2['x']}")
|
||||
print(f" y index: {result2['y']}")
|
||||
print(f" z index: {result2['z']}")
|
||||
|
||||
# Assert 验证 bottle2 (A2) 的结果
|
||||
assert result2['identifier'] == 'A2', f"Expected identifier 'A2', got '{result2['identifier']}'"
|
||||
assert result2['idx'] == 1, f"Expected idx 1, got {result2['idx']}"
|
||||
assert result2['x'] == 1, f"Expected x index 1, got {result2['x']}"
|
||||
assert result2['y'] == 0, f"Expected y index 0, got {result2['y']}"
|
||||
assert result2['z'] == 0, f"Expected z index 0, got {result2['z']}"
|
||||
print(" ✓ bottle2 (A2) 测试通过")
|
||||
print()
|
||||
|
||||
# 测试获取bottle3的标识符信息 (A3 = idx:2, x:2, y:0, z:0)
|
||||
result3 = carrier.get_child_identifier(bottle3)
|
||||
print("测试bottle3 (A3):")
|
||||
print(f" identifier: {result3['identifier']}")
|
||||
print(f" idx: {result3['idx']}")
|
||||
print(f" x index: {result3['x']}")
|
||||
print(f" y index: {result3['y']}")
|
||||
print(f" z index: {result3['z']}")
|
||||
|
||||
# Assert 验证 bottle3 (A3) 的结果
|
||||
assert result3['identifier'] == 'A3', f"Expected identifier 'A3', got '{result3['identifier']}'"
|
||||
assert result3['idx'] == 2, f"Expected idx 2, got {result3['idx']}"
|
||||
assert result3['x'] == 2, f"Expected x index 2, got {result3['x']}"
|
||||
assert result3['y'] == 0, f"Expected y index 0, got {result3['y']}"
|
||||
assert result3['z'] == 0, f"Expected z index 0, got {result3['z']}"
|
||||
print(" ✓ bottle3 (A3) 测试通过")
|
||||
print()
|
||||
|
||||
# 测试错误情况:查找不存在的资源
|
||||
bottle_not_exists = Bottle("bottle_not_exists", diameter=25.0, height=50.0, max_volume=15.0)
|
||||
try:
|
||||
carrier.get_child_identifier(bottle_not_exists)
|
||||
assert False, "应该抛出 ValueError 异常"
|
||||
except ValueError as e:
|
||||
print("✓ 正确抛出了 ValueError 异常:", str(e))
|
||||
assert "is not assigned to this carrier" in str(e), "异常消息应该包含预期的文本"
|
||||
|
||||
print("\n🎉 所有测试都通过了!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_get_child_identifier_with_indices()
|
||||
@@ -1,68 +0,0 @@
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
|
||||
from pylabrobot.resources import Resource as ResourcePLR
|
||||
from unilabos.resources.graphio import resource_bioyond_to_plr
|
||||
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
|
||||
from unilabos.registry.registry import lab_registry
|
||||
|
||||
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
||||
|
||||
lab_registry.setup()
|
||||
|
||||
|
||||
type_mapping = {
|
||||
"烧杯": ("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"),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bioyond_materials_reaction() -> list[dict]:
|
||||
print("加载 BioYond 物料数据...")
|
||||
print(os.getcwd())
|
||||
with open("bioyond_materials_reaction.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bioyond_materials_liquidhandling_1() -> list[dict]:
|
||||
print("加载 BioYond 物料数据...")
|
||||
print(os.getcwd())
|
||||
with open("bioyond_materials_liquidhandling_1.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bioyond_materials_liquidhandling_2() -> list[dict]:
|
||||
print("加载 BioYond 物料数据...")
|
||||
print(os.getcwd())
|
||||
with open("bioyond_materials_liquidhandling_2.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("materials_fixture", [
|
||||
"bioyond_materials_reaction",
|
||||
"bioyond_materials_liquidhandling_1",
|
||||
])
|
||||
def test_resourcetreeset_from_plr(materials_fixture, request) -> list[dict]:
|
||||
materials = request.getfixturevalue(materials_fixture)
|
||||
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
|
||||
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
|
||||
print(deck.summary())
|
||||
|
||||
r = ResourceTreeSet.from_plr_resources([deck])
|
||||
print(r.dump())
|
||||
# json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)
|
||||
@@ -1 +0,0 @@
|
||||
# 消息转换器测试包
|
||||
@@ -1,71 +0,0 @@
|
||||
"""
|
||||
基本测试
|
||||
|
||||
测试消息转换器的基本功能,包括导入、类型映射等。
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from unilabos.ros.msgs.message_converter import (
|
||||
msg_converter_manager,
|
||||
get_msg_type,
|
||||
get_action_type,
|
||||
get_ros_type_by_msgname,
|
||||
Point3D,
|
||||
Point,
|
||||
Float64,
|
||||
String,
|
||||
Bool,
|
||||
Int32,
|
||||
)
|
||||
|
||||
|
||||
class TestBasicFunctionality(unittest.TestCase):
|
||||
"""测试消息转换器的基本功能"""
|
||||
|
||||
def test_manager_initialization(self):
|
||||
"""测试导入管理器初始化"""
|
||||
self.assertIsNotNone(msg_converter_manager)
|
||||
self.assertTrue(len(msg_converter_manager.list_modules()) > 0)
|
||||
self.assertTrue(len(msg_converter_manager.list_classes()) > 0)
|
||||
|
||||
def test_get_msg_type(self):
|
||||
"""测试获取消息类型"""
|
||||
self.assertEqual(get_msg_type(float), Float64)
|
||||
self.assertEqual(get_msg_type(str), String)
|
||||
self.assertEqual(get_msg_type(bool), Bool)
|
||||
self.assertEqual(get_msg_type(int), Int32)
|
||||
self.assertEqual(get_msg_type(Point3D), Point)
|
||||
|
||||
# 测试错误情况
|
||||
with self.assertRaises(ValueError):
|
||||
get_msg_type(set) # 不支持的类型
|
||||
|
||||
def test_get_action_type(self):
|
||||
"""测试获取动作类型"""
|
||||
float_action = get_action_type(float)
|
||||
self.assertIsNotNone(float_action)
|
||||
self.assertTrue("type" in float_action)
|
||||
self.assertTrue("goal" in float_action)
|
||||
self.assertTrue("feedback" in float_action)
|
||||
|
||||
# 测试错误情况
|
||||
with self.assertRaises(ValueError):
|
||||
get_action_type(set) # 不支持的类型
|
||||
|
||||
def test_get_ros_type_by_msgname(self):
|
||||
"""测试通过消息名称获取ROS类型"""
|
||||
# 测试有效的消息名称
|
||||
point_type = get_ros_type_by_msgname("geometry_msgs/msg/Point")
|
||||
self.assertEqual(point_type, Point)
|
||||
|
||||
# 测试无效的消息名称
|
||||
with self.assertRaises(ValueError):
|
||||
get_ros_type_by_msgname("invalid_format")
|
||||
|
||||
# 不存在的消息类型可能会引发ImportError,但这依赖于运行环境
|
||||
# 因此不进行显式测试
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,131 +0,0 @@
|
||||
"""
|
||||
转换测试
|
||||
|
||||
测试Python对象和ROS消息之间的转换功能。
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from dataclasses import dataclass
|
||||
|
||||
from unilabos.ros.msgs.message_converter import (
|
||||
convert_to_ros_msg,
|
||||
convert_from_ros_msg,
|
||||
convert_to_ros_msg_with_mapping,
|
||||
convert_from_ros_msg_with_mapping,
|
||||
Point,
|
||||
Float64,
|
||||
String,
|
||||
Point3D,
|
||||
Resource,
|
||||
)
|
||||
|
||||
|
||||
# 定义一些测试数据类
|
||||
@dataclass
|
||||
class TestPoint:
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
z: float = 0.0
|
||||
|
||||
|
||||
class TestBasicConversion(unittest.TestCase):
|
||||
"""测试基本类型转换"""
|
||||
|
||||
def test_primitive_conversion(self):
|
||||
"""测试原始类型转换"""
|
||||
# Float转换
|
||||
float_value = 3.14
|
||||
ros_float = convert_to_ros_msg(Float64, float_value)
|
||||
self.assertEqual(ros_float.data, float_value)
|
||||
|
||||
# 反向转换
|
||||
py_float = convert_from_ros_msg(ros_float)
|
||||
self.assertEqual(py_float, float_value)
|
||||
|
||||
# 字符串转换
|
||||
str_value = "hello"
|
||||
ros_str = convert_to_ros_msg(String, str_value)
|
||||
self.assertEqual(ros_str.data, str_value)
|
||||
|
||||
# 反向转换
|
||||
py_str = convert_from_ros_msg(ros_str)
|
||||
self.assertEqual(py_str, str_value)
|
||||
|
||||
def test_point_conversion(self):
|
||||
"""测试点类型转换"""
|
||||
# 创建Point3D对象
|
||||
py_point = Point3D(x=1.0, y=2.0, z=3.0)
|
||||
|
||||
# 转换为ROS Point
|
||||
ros_point = convert_to_ros_msg(Point, py_point)
|
||||
self.assertEqual(ros_point.x, py_point.x)
|
||||
self.assertEqual(ros_point.y, py_point.y)
|
||||
self.assertEqual(ros_point.z, py_point.z)
|
||||
|
||||
# 反向转换
|
||||
py_point_back = convert_from_ros_msg(ros_point)
|
||||
self.assertEqual(py_point_back.x, py_point.x)
|
||||
self.assertEqual(py_point_back.y, py_point.y)
|
||||
self.assertEqual(py_point_back.z, py_point.z)
|
||||
|
||||
def test_dataclass_conversion(self):
|
||||
"""测试dataclass转换"""
|
||||
# 创建dataclass
|
||||
test_point = TestPoint(x=1.0, y=2.0, z=3.0)
|
||||
|
||||
# 转换
|
||||
ros_point = convert_to_ros_msg(Point, test_point)
|
||||
self.assertEqual(ros_point.x, test_point.x)
|
||||
self.assertEqual(ros_point.y, test_point.y)
|
||||
self.assertEqual(ros_point.z, test_point.z)
|
||||
|
||||
|
||||
class TestMappingConversion(unittest.TestCase):
|
||||
"""测试映射转换功能"""
|
||||
|
||||
def test_mapping_conversion(self):
|
||||
"""测试带映射的转换"""
|
||||
# 创建测试数据
|
||||
test_data = {
|
||||
"position": {"x": 1.0, "y": 2.0, "z": 3.0},
|
||||
"name": "test_resource",
|
||||
"id": "123",
|
||||
"type": "test_type",
|
||||
}
|
||||
|
||||
# 定义映射
|
||||
mapping = {
|
||||
"id": "id",
|
||||
"name": "name",
|
||||
"type": "type",
|
||||
"pose.position": "position",
|
||||
}
|
||||
|
||||
# 转换为ROS资源
|
||||
ros_resource = convert_to_ros_msg_with_mapping(Resource, test_data, mapping)
|
||||
self.assertEqual(ros_resource.id, "123")
|
||||
self.assertEqual(ros_resource.name, "test_resource")
|
||||
self.assertEqual(ros_resource.type, "test_type")
|
||||
self.assertEqual(ros_resource.pose.position.x, 1.0)
|
||||
self.assertEqual(ros_resource.pose.position.y, 2.0)
|
||||
self.assertEqual(ros_resource.pose.position.z, 3.0)
|
||||
|
||||
# 反向转换
|
||||
reverse_mapping = {
|
||||
"id": "id",
|
||||
"name": "name",
|
||||
"type": "type",
|
||||
"pose.position": "position",
|
||||
}
|
||||
|
||||
py_data = convert_from_ros_msg_with_mapping(ros_resource, reverse_mapping)
|
||||
self.assertEqual(py_data["id"], "123")
|
||||
self.assertEqual(py_data["name"], "test_resource")
|
||||
self.assertEqual(py_data["type"], "test_type")
|
||||
self.assertEqual(py_data["position"].x, 1.0)
|
||||
self.assertEqual(py_data["position"].y, 2.0)
|
||||
self.assertEqual(py_data["position"].z, 3.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,120 +0,0 @@
|
||||
"""
|
||||
映射测试
|
||||
|
||||
测试消息类型映射和字段映射功能。
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from dataclasses import dataclass
|
||||
|
||||
from unilabos.ros.msgs.message_converter import (
|
||||
_msg_mapping,
|
||||
_action_mapping,
|
||||
_msg_converter,
|
||||
_msg_converter_back,
|
||||
compare_model_fields,
|
||||
Point,
|
||||
Point3D,
|
||||
Float64,
|
||||
String,
|
||||
set_msg_data,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestMappingModel:
|
||||
"""用于测试映射的数据类"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
value: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestPointModel:
|
||||
"""用于测试字段比较的点模型"""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
|
||||
|
||||
class TestTypeMapping(unittest.TestCase):
|
||||
"""测试类型映射"""
|
||||
|
||||
def test_msg_mapping(self):
|
||||
"""测试消息类型映射"""
|
||||
self.assertIn(float, _msg_mapping)
|
||||
self.assertEqual(_msg_mapping[float], Float64)
|
||||
|
||||
self.assertIn(str, _msg_mapping)
|
||||
self.assertEqual(_msg_mapping[str], String)
|
||||
|
||||
self.assertIn(Point3D, _msg_mapping)
|
||||
self.assertEqual(_msg_mapping[Point3D], Point)
|
||||
|
||||
def test_action_mapping(self):
|
||||
"""测试动作类型映射"""
|
||||
self.assertIn(float, _action_mapping)
|
||||
self.assertIn("type", _action_mapping[float])
|
||||
self.assertIn("goal", _action_mapping[float])
|
||||
self.assertIn("feedback", _action_mapping[float])
|
||||
self.assertIn("result", _action_mapping[float])
|
||||
|
||||
def test_converter_mapping(self):
|
||||
"""测试转换器映射"""
|
||||
# 测试Python到ROS映射
|
||||
self.assertIn(float, _msg_converter)
|
||||
self.assertIn(Float64, _msg_converter)
|
||||
self.assertIn(String, _msg_converter)
|
||||
self.assertIn(Point, _msg_converter)
|
||||
|
||||
# 测试ROS到Python映射
|
||||
self.assertIn(float, _msg_converter_back)
|
||||
self.assertIn(Float64, _msg_converter_back)
|
||||
self.assertIn(String, _msg_converter_back)
|
||||
self.assertIn(Point, _msg_converter_back)
|
||||
|
||||
|
||||
class TestFieldMapping(unittest.TestCase):
|
||||
"""测试字段映射"""
|
||||
|
||||
def test_compare_model_fields(self):
|
||||
"""测试模型字段比较"""
|
||||
# Point3D和TestPointModel有相同的字段
|
||||
self.assertTrue(compare_model_fields(Point3D, TestPointModel))
|
||||
|
||||
# 与其他类型比较
|
||||
self.assertFalse(compare_model_fields(Point3D, TestMappingModel))
|
||||
self.assertFalse(compare_model_fields(Point3D, float))
|
||||
|
||||
# 类型对象和实例对象比较
|
||||
point = Point3D(x=1.0, y=2.0, z=3.0)
|
||||
self.assertTrue(compare_model_fields(Point3D, type(point)))
|
||||
|
||||
def test_set_msg_data(self):
|
||||
"""测试设置消息数据类型"""
|
||||
# 测试float转换
|
||||
float_value = "3.14"
|
||||
self.assertEqual(set_msg_data("float", float_value), 3.14)
|
||||
self.assertEqual(set_msg_data("double", float_value), 3.14)
|
||||
|
||||
# 测试int转换
|
||||
int_value = "42"
|
||||
self.assertEqual(set_msg_data("int", int_value), 42)
|
||||
|
||||
# 测试bool转换
|
||||
bool_value = "True"
|
||||
self.assertEqual(set_msg_data("bool", bool_value), True)
|
||||
|
||||
# 测试str转换
|
||||
str_value = "hello"
|
||||
self.assertEqual(set_msg_data("str", str_value), "hello")
|
||||
|
||||
# 测试默认转换
|
||||
default_value = 123
|
||||
self.assertEqual(set_msg_data("unknown_type", default_value), "123")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,47 +0,0 @@
|
||||
"""
|
||||
测试运行器
|
||||
|
||||
运行所有消息转换器的测试用例。
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
||||
|
||||
# 导入测试模块
|
||||
from test.ros.msgs.test_basic import TestBasicFunctionality
|
||||
from test.ros.msgs.test_conversion import TestBasicConversion, TestMappingConversion
|
||||
from test.ros.msgs.test_mapping import TestTypeMapping, TestFieldMapping
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""运行所有测试"""
|
||||
# 创建测试加载器
|
||||
loader = unittest.TestLoader()
|
||||
|
||||
# 创建测试套件
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# 添加测试类
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestBasicFunctionality))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestBasicConversion))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestMappingConversion))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestTypeMapping))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestFieldMapping))
|
||||
|
||||
# 创建测试运行器
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
|
||||
# 运行测试
|
||||
result = runner.run(suite)
|
||||
|
||||
# 返回结果
|
||||
return result.wasSuccessful()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_tests()
|
||||
sys.exit(not success)
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"workflow": [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_1",
|
||||
"targets": "Liquid_2",
|
||||
"asp_vol": 66.0,
|
||||
"dis_vol": 66.0,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 94.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_2",
|
||||
"targets": "Liquid_3",
|
||||
"asp_vol": 58.0,
|
||||
"dis_vol": 96.0,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 94.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_4",
|
||||
"targets": "Liquid_2",
|
||||
"asp_vol": 85.0,
|
||||
"dis_vol": 170.0,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 94.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_4",
|
||||
"targets": "Liquid_2",
|
||||
"asp_vol": 63.333333333333336,
|
||||
"dis_vol": 170.0,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 94.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_2",
|
||||
"targets": "Liquid_3",
|
||||
"asp_vol": 72.0,
|
||||
"dis_vol": 150.0,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 94.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_4",
|
||||
"targets": "Liquid_2",
|
||||
"asp_vol": 85.0,
|
||||
"dis_vol": 170.0,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 94.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_4",
|
||||
"targets": "Liquid_2",
|
||||
"asp_vol": 63.333333333333336,
|
||||
"dis_vol": 170.0,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 94.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_2",
|
||||
"targets": "Liquid_3",
|
||||
"asp_vol": 72.0,
|
||||
"dis_vol": 150.0,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 94.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_2",
|
||||
"targets": "Liquid_3",
|
||||
"asp_vol": 20.0,
|
||||
"dis_vol": 20.0,
|
||||
"asp_flow_rate": 7.6,
|
||||
"dis_flow_rate": 7.6
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_5",
|
||||
"targets": "Liquid_2",
|
||||
"asp_vol": 6.0,
|
||||
"dis_vol": 12.0,
|
||||
"asp_flow_rate": 7.6,
|
||||
"dis_flow_rate": 7.6
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_5",
|
||||
"targets": "Liquid_2",
|
||||
"asp_vol": 10.666666666666666,
|
||||
"dis_vol": 12.0,
|
||||
"asp_flow_rate": 7.599999999999999,
|
||||
"dis_flow_rate": 7.6
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "Liquid_2",
|
||||
"targets": "Liquid_6",
|
||||
"asp_vol": 12.0,
|
||||
"dis_vol": 10.0,
|
||||
"asp_flow_rate": 7.6,
|
||||
"dis_flow_rate": 7.6
|
||||
}
|
||||
}
|
||||
],
|
||||
"reagent": {
|
||||
"Liquid_6": {
|
||||
"slot": 1,
|
||||
"well": [
|
||||
"A2"
|
||||
],
|
||||
"labware": "elution plate"
|
||||
},
|
||||
"Liquid_1": {
|
||||
"slot": 2,
|
||||
"well": [
|
||||
"A1",
|
||||
"A2",
|
||||
"A4"
|
||||
],
|
||||
"labware": "reagent reservoir"
|
||||
},
|
||||
"Liquid_4": {
|
||||
"slot": 2,
|
||||
"well": [
|
||||
"A1",
|
||||
"A2",
|
||||
"A4"
|
||||
],
|
||||
"labware": "reagent reservoir"
|
||||
},
|
||||
"Liquid_5": {
|
||||
"slot": 2,
|
||||
"well": [
|
||||
"A1",
|
||||
"A2",
|
||||
"A4"
|
||||
],
|
||||
"labware": "reagent reservoir"
|
||||
},
|
||||
"Liquid_2": {
|
||||
"slot": 4,
|
||||
"well": [
|
||||
"A2"
|
||||
],
|
||||
"labware": "TAG1 plate on Magnetic Module GEN2"
|
||||
},
|
||||
"Liquid_3": {
|
||||
"slot": 12,
|
||||
"well": [
|
||||
"A1"
|
||||
],
|
||||
"labware": "Opentrons Fixed Trash"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 148 KiB |
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"steps_info": [
|
||||
{
|
||||
"step_number": 1,
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"source": "sample supernatant",
|
||||
"target": "antibody-coated well",
|
||||
"volume": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_number": 2,
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"source": "washing buffer",
|
||||
"target": "antibody-coated well",
|
||||
"volume": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_number": 3,
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"source": "washing buffer",
|
||||
"target": "antibody-coated well",
|
||||
"volume": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_number": 4,
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"source": "washing buffer",
|
||||
"target": "antibody-coated well",
|
||||
"volume": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_number": 5,
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"source": "TMB substrate",
|
||||
"target": "antibody-coated well",
|
||||
"volume": 100
|
||||
}
|
||||
}
|
||||
],
|
||||
"labware_info": [
|
||||
{"reagent_name": "sample supernatant", "material_name": "96深孔板", "positions": 1},
|
||||
{"reagent_name": "washing buffer", "material_name": "储液槽", "positions": 2},
|
||||
{"reagent_name": "TMB substrate", "material_name": "储液槽", "positions": 3},
|
||||
{"reagent_name": "antibody-coated well", "material_name": "96 细胞培养皿", "positions": 4},
|
||||
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 5},
|
||||
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 6},
|
||||
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 7},
|
||||
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 8},
|
||||
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 9},
|
||||
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 10},
|
||||
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 11},
|
||||
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 13}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 117 KiB |
@@ -1,94 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
|
||||
def _normalize_steps(data):
|
||||
normalized = []
|
||||
for step in data:
|
||||
action = step.get("action") or step.get("operation")
|
||||
if not action:
|
||||
continue
|
||||
raw_params = step.get("parameters") or step.get("action_args") or {}
|
||||
params = dict(raw_params)
|
||||
|
||||
if "source" in raw_params and "sources" not in raw_params:
|
||||
params["sources"] = raw_params["source"]
|
||||
if "target" in raw_params and "targets" not in raw_params:
|
||||
params["targets"] = raw_params["target"]
|
||||
|
||||
description = step.get("description") or step.get("purpose")
|
||||
step_dict = {"action": action, "parameters": params}
|
||||
if description:
|
||||
step_dict["description"] = description
|
||||
normalized.append(step_dict)
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_labware(data):
|
||||
labware = {}
|
||||
for item in data:
|
||||
reagent_name = item.get("reagent_name")
|
||||
key = reagent_name or item.get("material_name") or item.get("name")
|
||||
if not key:
|
||||
continue
|
||||
key = str(key)
|
||||
idx = 1
|
||||
original_key = key
|
||||
while key in labware:
|
||||
idx += 1
|
||||
key = f"{original_key}_{idx}"
|
||||
|
||||
labware[key] = {
|
||||
"slot": item.get("positions") or item.get("slot"),
|
||||
"labware": item.get("material_name") or item.get("labware"),
|
||||
"well": item.get("well", []),
|
||||
"type": item.get("type", "reagent"),
|
||||
"role": item.get("role", ""),
|
||||
"name": key,
|
||||
}
|
||||
return labware
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protocol_name", [
|
||||
"example_bio",
|
||||
# "bioyond_materials_liquidhandling_1",
|
||||
"example_prcxi",
|
||||
])
|
||||
def test_build_protocol_graph(protocol_name):
|
||||
data_path = Path(__file__).with_name(f"{protocol_name}.json")
|
||||
with data_path.open("r", encoding="utf-8") as fp:
|
||||
d = json.load(fp)
|
||||
|
||||
if "workflow" in d and "reagent" in d:
|
||||
protocol_steps = d["workflow"]
|
||||
labware_info = d["reagent"]
|
||||
elif "steps_info" in d and "labware_info" in d:
|
||||
protocol_steps = _normalize_steps(d["steps_info"])
|
||||
labware_info = _normalize_labware(d["labware_info"])
|
||||
else:
|
||||
raise ValueError("Unsupported protocol format")
|
||||
|
||||
graph = build_protocol_graph(
|
||||
labware_info=labware_info,
|
||||
protocol_steps=protocol_steps,
|
||||
workstation_name="PRCXi",
|
||||
)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
||||
output_path = data_path.with_name(f"{protocol_name}_graph_{timestamp}.png")
|
||||
draw_protocol_graph_with_ports(graph, str(output_path))
|
||||
print(graph)
|
||||
0
unilabos/workflow/__init__.py
Normal file
0
unilabos/workflow/__init__.py
Normal file
547
unilabos/workflow/common.py
Normal file
547
unilabos/workflow/common.py
Normal file
@@ -0,0 +1,547 @@
|
||||
import re
|
||||
import uuid
|
||||
|
||||
import networkx as nx
|
||||
from networkx.drawing.nx_agraph import to_agraph
|
||||
import matplotlib.pyplot as plt
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
|
||||
Json = Dict[str, Any]
|
||||
|
||||
# ---------------- Graph ----------------
|
||||
|
||||
|
||||
class WorkflowGraph:
|
||||
"""简单的有向图实现:使用 params 单层参数;inputs 内含连线;支持 node-link 导出"""
|
||||
|
||||
def __init__(self):
|
||||
self.nodes: Dict[str, Dict[str, Any]] = {}
|
||||
self.edges: List[Dict[str, Any]] = []
|
||||
|
||||
def add_node(self, node_id: str, **attrs):
|
||||
self.nodes[node_id] = attrs
|
||||
|
||||
def add_edge(self, source: str, target: str, **attrs):
|
||||
# 将 source_port/target_port 映射为服务端期望的 source_handle_key/target_handle_key
|
||||
source_handle_key = attrs.pop("source_port", "") or attrs.pop("source_handle_key", "")
|
||||
target_handle_key = attrs.pop("target_port", "") or attrs.pop("target_handle_key", "")
|
||||
|
||||
edge = {
|
||||
"source": source,
|
||||
"target": target,
|
||||
"source_node_uuid": source,
|
||||
"target_node_uuid": target,
|
||||
"source_handle_key": source_handle_key,
|
||||
"source_handle_io": attrs.pop("source_handle_io", "source"),
|
||||
"target_handle_key": target_handle_key,
|
||||
"target_handle_io": attrs.pop("target_handle_io", "target"),
|
||||
**attrs,
|
||||
}
|
||||
self.edges.append(edge)
|
||||
|
||||
def _materialize_wiring_into_inputs(
|
||||
self,
|
||||
obj: Any,
|
||||
inputs: Dict[str, Any],
|
||||
variable_sources: Dict[str, Dict[str, Any]],
|
||||
target_node_id: str,
|
||||
base_path: List[str],
|
||||
):
|
||||
has_var = False
|
||||
|
||||
def walk(node: Any, path: List[str]):
|
||||
nonlocal has_var
|
||||
if isinstance(node, dict):
|
||||
if "__var__" in node:
|
||||
has_var = True
|
||||
varname = node["__var__"]
|
||||
placeholder = f"${{{varname}}}"
|
||||
src = variable_sources.get(varname)
|
||||
if src:
|
||||
key = ".".join(path) # e.g. "params.foo.bar.0"
|
||||
inputs[key] = {"node": src["node_id"], "output": src.get("output_name", "result")}
|
||||
self.add_edge(
|
||||
str(src["node_id"]),
|
||||
target_node_id,
|
||||
source_handle_io=src.get("output_name", "result"),
|
||||
target_handle_io=key,
|
||||
)
|
||||
return placeholder
|
||||
return {k: walk(v, path + [k]) for k, v in node.items()}
|
||||
if isinstance(node, list):
|
||||
return [walk(v, path + [str(i)]) for i, v in enumerate(node)]
|
||||
return node
|
||||
|
||||
replaced = walk(obj, base_path[:])
|
||||
return replaced, has_var
|
||||
|
||||
def add_workflow_node(
|
||||
self,
|
||||
node_id: int,
|
||||
*,
|
||||
device_key: Optional[str] = None, # 实例名,如 "ser"
|
||||
resource_name: Optional[str] = None, # registry key(原 device_class)
|
||||
module: Optional[str] = None,
|
||||
template_name: Optional[str] = None, # 动作/模板名(原 action_key)
|
||||
params: Dict[str, Any],
|
||||
variable_sources: Dict[str, Dict[str, Any]],
|
||||
add_ready_if_no_vars: bool = True,
|
||||
prev_node_id: Optional[int] = None,
|
||||
**extra_attrs,
|
||||
) -> None:
|
||||
"""添加工作流节点:params 单层;自动变量连线与 ready 串联;支持附加属性"""
|
||||
node_id_str = str(node_id)
|
||||
inputs: Dict[str, Any] = {}
|
||||
|
||||
params, has_var = self._materialize_wiring_into_inputs(
|
||||
params, inputs, variable_sources, node_id_str, base_path=["params"]
|
||||
)
|
||||
|
||||
if add_ready_if_no_vars and not has_var:
|
||||
last_id = str(prev_node_id) if prev_node_id is not None else "-1"
|
||||
inputs["ready"] = {"node": int(last_id), "output": "ready"}
|
||||
self.add_edge(last_id, node_id_str, source_handle_io="ready", target_handle_io="ready")
|
||||
|
||||
node_obj = {
|
||||
"device_key": device_key,
|
||||
"resource_name": resource_name, # ✅ 新名字
|
||||
"module": module,
|
||||
"template_name": template_name, # ✅ 新名字
|
||||
"params": params,
|
||||
"inputs": inputs,
|
||||
}
|
||||
node_obj.update(extra_attrs or {})
|
||||
self.add_node(node_id_str, parameters=node_obj)
|
||||
|
||||
# 顺序工作流导出(连线在 inputs,不返回 edges)
|
||||
def to_dict(self) -> List[Dict[str, Any]]:
|
||||
result = []
|
||||
for node_id, attrs in self.nodes.items():
|
||||
node = {"uuid": node_id}
|
||||
params = dict(attrs.get("parameters", {}) or {})
|
||||
flat = {k: v for k, v in attrs.items() if k != "parameters"}
|
||||
flat.update(params)
|
||||
node.update(flat)
|
||||
result.append(node)
|
||||
return sorted(result, key=lambda n: int(n["uuid"]) if str(n["uuid"]).isdigit() else n["uuid"])
|
||||
|
||||
# node-link 导出(含 edges)
|
||||
def to_node_link_dict(self) -> Dict[str, Any]:
|
||||
nodes_list = []
|
||||
for node_id, attrs in self.nodes.items():
|
||||
node_attrs = attrs.copy()
|
||||
params = node_attrs.pop("parameters", {}) or {}
|
||||
node_attrs.update(params)
|
||||
nodes_list.append({"uuid": node_id, **node_attrs})
|
||||
return {
|
||||
"directed": True,
|
||||
"multigraph": False,
|
||||
"graph": {},
|
||||
"nodes": nodes_list,
|
||||
"edges": self.edges,
|
||||
"links": self.edges,
|
||||
}
|
||||
|
||||
|
||||
def refactor_data(
|
||||
data: List[Dict[str, Any]],
|
||||
action_resource_mapping: Optional[Dict[str, str]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""统一的数据重构函数,根据操作类型自动选择模板
|
||||
|
||||
Args:
|
||||
data: 原始步骤数据列表
|
||||
action_resource_mapping: action 到 resource_name 的映射字典,可选
|
||||
"""
|
||||
refactored_data = []
|
||||
|
||||
# 定义操作映射,包含生物实验和有机化学的所有操作
|
||||
OPERATION_MAPPING = {
|
||||
# 生物实验操作
|
||||
"transfer_liquid": "transfer_liquid",
|
||||
"transfer": "transfer",
|
||||
"incubation": "incubation",
|
||||
"move_labware": "move_labware",
|
||||
"oscillation": "oscillation",
|
||||
# 有机化学操作
|
||||
"HeatChillToTemp": "HeatChillProtocol",
|
||||
"StopHeatChill": "HeatChillStopProtocol",
|
||||
"StartHeatChill": "HeatChillStartProtocol",
|
||||
"HeatChill": "HeatChillProtocol",
|
||||
"Dissolve": "DissolveProtocol",
|
||||
"Transfer": "TransferProtocol",
|
||||
"Evaporate": "EvaporateProtocol",
|
||||
"Recrystallize": "RecrystallizeProtocol",
|
||||
"Filter": "FilterProtocol",
|
||||
"Dry": "DryProtocol",
|
||||
"Add": "AddProtocol",
|
||||
}
|
||||
|
||||
UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"]
|
||||
|
||||
for step in data:
|
||||
operation = step.get("action")
|
||||
if not operation or operation in UNSUPPORTED_OPERATIONS:
|
||||
continue
|
||||
|
||||
# 处理重复操作
|
||||
if operation == "Repeat":
|
||||
times = step.get("times", step.get("parameters", {}).get("times", 1))
|
||||
sub_steps = step.get("steps", step.get("parameters", {}).get("steps", []))
|
||||
for i in range(int(times)):
|
||||
sub_data = refactor_data(sub_steps, action_resource_mapping)
|
||||
refactored_data.extend(sub_data)
|
||||
continue
|
||||
|
||||
# 获取模板名称
|
||||
template_name = OPERATION_MAPPING.get(operation)
|
||||
if not template_name:
|
||||
# 自动推断模板类型
|
||||
if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]:
|
||||
template_name = f"biomek-{operation}"
|
||||
else:
|
||||
template_name = f"{operation}Protocol"
|
||||
|
||||
# 获取 resource_name
|
||||
resource_name = f"device.{operation.lower()}"
|
||||
if action_resource_mapping:
|
||||
resource_name = action_resource_mapping.get(operation, resource_name)
|
||||
|
||||
# 获取步骤编号,生成 name 字段
|
||||
step_number = step.get("step_number")
|
||||
name = f"Step {step_number}" if step_number is not None else None
|
||||
|
||||
# 创建步骤数据
|
||||
step_data = {
|
||||
"template_name": template_name,
|
||||
"resource_name": resource_name,
|
||||
"description": step.get("description", step.get("purpose", f"{operation} operation")),
|
||||
"lab_node_type": "Device",
|
||||
"param": step.get("parameters", step.get("action_args", {})),
|
||||
"footer": f"{template_name}-{resource_name}",
|
||||
}
|
||||
if name:
|
||||
step_data["name"] = name
|
||||
refactored_data.append(step_data)
|
||||
|
||||
return refactored_data
|
||||
|
||||
|
||||
def build_protocol_graph(
|
||||
labware_info: List[Dict[str, Any]],
|
||||
protocol_steps: List[Dict[str, Any]],
|
||||
workstation_name: str,
|
||||
action_resource_mapping: Optional[Dict[str, str]] = None,
|
||||
) -> WorkflowGraph:
|
||||
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
||||
|
||||
Args:
|
||||
labware_info: labware 信息字典
|
||||
protocol_steps: 协议步骤列表
|
||||
workstation_name: 工作站名称
|
||||
action_resource_mapping: action 到 resource_name 的映射字典,可选
|
||||
"""
|
||||
G = WorkflowGraph()
|
||||
resource_last_writer = {}
|
||||
|
||||
protocol_steps = refactor_data(protocol_steps, action_resource_mapping)
|
||||
# 有机化学&移液站协议图构建
|
||||
WORKSTATION_ID = workstation_name
|
||||
|
||||
# 为所有labware创建资源节点
|
||||
res_index = 0
|
||||
for labware_id, item in labware_info.items():
|
||||
# item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}")
|
||||
node_id = str(uuid.uuid4())
|
||||
|
||||
# 判断节点类型
|
||||
if "Rack" in str(labware_id) or "Tip" in str(labware_id):
|
||||
lab_node_type = "Labware"
|
||||
description = f"Prepare Labware: {labware_id}"
|
||||
liquid_type = []
|
||||
liquid_volume = []
|
||||
elif item.get("type") == "hardware" or "reactor" in str(labware_id).lower():
|
||||
if "reactor" not in str(labware_id).lower():
|
||||
continue
|
||||
lab_node_type = "Sample"
|
||||
description = f"Prepare Reactor: {labware_id}"
|
||||
liquid_type = []
|
||||
liquid_volume = []
|
||||
else:
|
||||
lab_node_type = "Reagent"
|
||||
description = f"Add Reagent to Flask: {labware_id}"
|
||||
liquid_type = [labware_id]
|
||||
liquid_volume = [1e5]
|
||||
|
||||
res_index += 1
|
||||
G.add_node(
|
||||
node_id,
|
||||
template_name="create_resource",
|
||||
resource_name="host_node",
|
||||
name=f"Res {res_index}",
|
||||
description=description,
|
||||
lab_node_type=lab_node_type,
|
||||
footer="create_resource-host_node",
|
||||
param={
|
||||
"res_id": labware_id,
|
||||
"device_id": WORKSTATION_ID,
|
||||
"class_name": "container",
|
||||
"parent": WORKSTATION_ID,
|
||||
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"liquid_input_slot": [-1],
|
||||
"liquid_type": liquid_type,
|
||||
"liquid_volume": liquid_volume,
|
||||
"slot_on_deck": "",
|
||||
},
|
||||
)
|
||||
resource_last_writer[labware_id] = f"{node_id}:labware"
|
||||
|
||||
last_control_node_id = None
|
||||
|
||||
# 处理协议步骤
|
||||
for step in protocol_steps:
|
||||
node_id = str(uuid.uuid4())
|
||||
G.add_node(node_id, **step)
|
||||
|
||||
# 控制流
|
||||
if last_control_node_id is not None:
|
||||
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
|
||||
last_control_node_id = node_id
|
||||
|
||||
# 物料流
|
||||
params = step.get("param", {})
|
||||
input_resources_possible_names = [
|
||||
"vessel",
|
||||
"to_vessel",
|
||||
"from_vessel",
|
||||
"reagent",
|
||||
"solvent",
|
||||
"compound",
|
||||
"sources",
|
||||
"targets",
|
||||
]
|
||||
|
||||
for target_port in input_resources_possible_names:
|
||||
resource_name = params.get(target_port)
|
||||
if resource_name and resource_name in resource_last_writer:
|
||||
source_node, source_port = resource_last_writer[resource_name].split(":")
|
||||
G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port)
|
||||
|
||||
output_resources = {
|
||||
"vessel_out": params.get("vessel"),
|
||||
"from_vessel_out": params.get("from_vessel"),
|
||||
"to_vessel_out": params.get("to_vessel"),
|
||||
"filtrate_out": params.get("filtrate_vessel"),
|
||||
"reagent": params.get("reagent"),
|
||||
"solvent": params.get("solvent"),
|
||||
"compound": params.get("compound"),
|
||||
"sources_out": params.get("sources"),
|
||||
"targets_out": params.get("targets"),
|
||||
}
|
||||
|
||||
for source_port, resource_name in output_resources.items():
|
||||
if resource_name:
|
||||
resource_last_writer[resource_name] = f"{node_id}:{source_port}"
|
||||
|
||||
return G
|
||||
|
||||
|
||||
def draw_protocol_graph(protocol_graph: WorkflowGraph, output_path: str):
|
||||
"""
|
||||
(辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。
|
||||
"""
|
||||
if not protocol_graph:
|
||||
print("Cannot draw graph: Graph object is empty.")
|
||||
return
|
||||
|
||||
G = nx.DiGraph()
|
||||
|
||||
for node_id, attrs in protocol_graph.nodes.items():
|
||||
label = attrs.get("description", attrs.get("template_name", node_id[:8]))
|
||||
G.add_node(node_id, label=label, **attrs)
|
||||
|
||||
for edge in protocol_graph.edges:
|
||||
G.add_edge(edge["source"], edge["target"])
|
||||
|
||||
plt.figure(figsize=(20, 15))
|
||||
try:
|
||||
pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
|
||||
except Exception:
|
||||
pos = nx.shell_layout(G) # Fallback layout
|
||||
|
||||
node_labels = {node: data["label"] for node, data in G.nodes(data=True)}
|
||||
nx.draw(
|
||||
G,
|
||||
pos,
|
||||
with_labels=False,
|
||||
node_size=2500,
|
||||
node_color="skyblue",
|
||||
node_shape="o",
|
||||
edge_color="gray",
|
||||
width=1.5,
|
||||
arrowsize=15,
|
||||
)
|
||||
nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold")
|
||||
|
||||
plt.title("Chemical Protocol Workflow Graph", size=15)
|
||||
plt.savefig(output_path, dpi=300, bbox_inches="tight")
|
||||
plt.close()
|
||||
print(f" - Visualization saved to '{output_path}'")
|
||||
|
||||
|
||||
COMPASS = {"n", "e", "s", "w", "ne", "nw", "se", "sw", "c"}
|
||||
|
||||
|
||||
def _is_compass(port: str) -> bool:
|
||||
return isinstance(port, str) and port.lower() in COMPASS
|
||||
|
||||
|
||||
def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"):
|
||||
"""
|
||||
使用 Graphviz 端口语法绘制协议工作流图。
|
||||
- 若边上的 source_port/target_port 是 compass(n/e/s/w/...),直接用 compass。
|
||||
- 否则自动为节点创建 record 形状并定义命名端口 <portname>。
|
||||
最终由 PyGraphviz 渲染并输出到 output_path(后缀决定格式,如 .png/.svg/.pdf)。
|
||||
"""
|
||||
if not protocol_graph:
|
||||
print("Cannot draw graph: Graph object is empty.")
|
||||
return
|
||||
|
||||
# 1) 先用 networkx 搭建有向图,保留端口属性
|
||||
G = nx.DiGraph()
|
||||
for node_id, attrs in protocol_graph.nodes.items():
|
||||
label = attrs.get("description", attrs.get("template_name", node_id[:8]))
|
||||
# 保留一个干净的“中心标签”,用于放在 record 的中间槽
|
||||
G.add_node(node_id, _core_label=str(label), **{k: v for k, v in attrs.items() if k not in ("label",)})
|
||||
|
||||
edges_data = []
|
||||
in_ports_by_node = {} # 收集命名输入端口
|
||||
out_ports_by_node = {} # 收集命名输出端口
|
||||
|
||||
for edge in protocol_graph.edges:
|
||||
u = edge["source"]
|
||||
v = edge["target"]
|
||||
sp = edge.get("source_handle_key") or edge.get("source_port")
|
||||
tp = edge.get("target_handle_key") or edge.get("target_port")
|
||||
|
||||
# 记录到图里(保留原始端口信息)
|
||||
G.add_edge(u, v, source_handle_key=sp, target_handle_key=tp)
|
||||
edges_data.append((u, v, sp, tp))
|
||||
|
||||
# 如果不是 compass,就按“命名端口”先归类,等会儿给节点造 record
|
||||
if sp and not _is_compass(sp):
|
||||
out_ports_by_node.setdefault(u, set()).add(str(sp))
|
||||
if tp and not _is_compass(tp):
|
||||
in_ports_by_node.setdefault(v, set()).add(str(tp))
|
||||
|
||||
# 2) 转为 AGraph,使用 Graphviz 渲染
|
||||
A = to_agraph(G)
|
||||
A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10")
|
||||
A.node_attr.update(
|
||||
shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica"
|
||||
)
|
||||
A.edge_attr.update(arrowsize="0.8", color="#666666")
|
||||
|
||||
# 3) 为需要命名端口的节点设置 record 形状与 label
|
||||
# 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口
|
||||
for n in A.nodes():
|
||||
node = A.get_node(n)
|
||||
core = G.nodes[n].get("_core_label", n)
|
||||
|
||||
in_ports = sorted(in_ports_by_node.get(n, []))
|
||||
out_ports = sorted(out_ports_by_node.get(n, []))
|
||||
|
||||
# 如果该节点涉及命名端口,则用 record;否则保留原 box
|
||||
if in_ports or out_ports:
|
||||
|
||||
def port_fields(ports):
|
||||
if not ports:
|
||||
return " " # 必须留一个空槽占位
|
||||
# 每个端口一个小格子,<p> name
|
||||
return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports)
|
||||
|
||||
left = port_fields(in_ports)
|
||||
right = port_fields(out_ports)
|
||||
|
||||
# 三栏:左(入) | 中(节点名) | 右(出)
|
||||
record_label = f"{{ {left} | {core} | {right} }}"
|
||||
node.attr.update(shape="record", label=record_label)
|
||||
else:
|
||||
# 没有命名端口:普通盒子,显示核心标签
|
||||
node.attr.update(label=str(core))
|
||||
|
||||
# 4) 给边设置 headport / tailport
|
||||
# - 若端口为 compass:直接用 compass(e.g., headport="e")
|
||||
# - 若端口为命名端口:使用在 record 中定义的 <port> 名(同名即可)
|
||||
for u, v, sp, tp in edges_data:
|
||||
e = A.get_edge(u, v)
|
||||
|
||||
# Graphviz 属性:tail 是源,head 是目标
|
||||
if sp:
|
||||
if _is_compass(sp):
|
||||
e.attr["tailport"] = sp.lower()
|
||||
else:
|
||||
# 与 record label 中 <port> 名一致;特殊字符已在 label 中做了清洗
|
||||
e.attr["tailport"] = re.sub(r"[^A-Za-z0-9_:.|-]", "_", str(sp))
|
||||
|
||||
if tp:
|
||||
if _is_compass(tp):
|
||||
e.attr["headport"] = tp.lower()
|
||||
else:
|
||||
e.attr["headport"] = re.sub(r"[^A-Za-z0-9_:.|-]", "_", str(tp))
|
||||
|
||||
# 可选:若想让边更贴边缘,可设置 constraint/spline 等
|
||||
# e.attr["arrowhead"] = "vee"
|
||||
|
||||
# 5) 输出
|
||||
A.draw(output_path, prog="dot")
|
||||
print(f" - Port-aware workflow rendered to '{output_path}'")
|
||||
|
||||
|
||||
# ---------------- Registry Adapter ----------------
|
||||
|
||||
|
||||
class RegistryAdapter:
|
||||
"""根据 module 的类名(冒号右侧)反查 registry 的 resource_name(原 device_class),并抽取参数顺序"""
|
||||
|
||||
def __init__(self, device_registry: Dict[str, Any]):
|
||||
self.device_registry = device_registry or {}
|
||||
self.module_class_to_resource = self._build_module_class_index()
|
||||
|
||||
def _build_module_class_index(self) -> Dict[str, str]:
|
||||
idx = {}
|
||||
for resource_name, info in self.device_registry.items():
|
||||
module = info.get("module")
|
||||
if isinstance(module, str) and ":" in module:
|
||||
cls = module.split(":")[-1]
|
||||
idx[cls] = resource_name
|
||||
idx[cls.lower()] = resource_name
|
||||
return idx
|
||||
|
||||
def resolve_resource_by_classname(self, class_name: str) -> Optional[str]:
|
||||
if not class_name:
|
||||
return None
|
||||
return self.module_class_to_resource.get(class_name) or self.module_class_to_resource.get(class_name.lower())
|
||||
|
||||
def get_device_module(self, resource_name: Optional[str]) -> Optional[str]:
|
||||
if not resource_name:
|
||||
return None
|
||||
return self.device_registry.get(resource_name, {}).get("module")
|
||||
|
||||
def get_actions(self, resource_name: Optional[str]) -> Dict[str, Any]:
|
||||
if not resource_name:
|
||||
return {}
|
||||
return (self.device_registry.get(resource_name, {}).get("class", {}).get("action_value_mappings", {})) or {}
|
||||
|
||||
def get_action_schema(self, resource_name: Optional[str], template_name: str) -> Optional[Json]:
|
||||
return (self.get_actions(resource_name).get(template_name) or {}).get("schema")
|
||||
|
||||
def get_action_goal_default(self, resource_name: Optional[str], template_name: str) -> Json:
|
||||
return (self.get_actions(resource_name).get(template_name) or {}).get("goal_default", {}) or {}
|
||||
|
||||
def get_action_input_keys(self, resource_name: Optional[str], template_name: str) -> List[str]:
|
||||
schema = self.get_action_schema(resource_name, template_name) or {}
|
||||
goal = (schema.get("properties") or {}).get("goal") or {}
|
||||
props = goal.get("properties") or {}
|
||||
required = goal.get("required") or []
|
||||
return list(dict.fromkeys(required + list(props.keys())))
|
||||
356
unilabos/workflow/convert_from_json.py
Normal file
356
unilabos/workflow/convert_from_json.py
Normal file
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
JSON 工作流转换模块
|
||||
|
||||
提供从多种 JSON 格式转换为统一工作流格式的功能。
|
||||
支持的格式:
|
||||
1. workflow/reagent 格式
|
||||
2. steps_info/labware_info 格式
|
||||
"""
|
||||
|
||||
import json
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from unilabos.workflow.common import WorkflowGraph, build_protocol_graph
|
||||
from unilabos.registry.registry import lab_registry
|
||||
|
||||
|
||||
def get_action_handles(resource_name: str, template_name: str) -> Dict[str, List[str]]:
|
||||
"""
|
||||
从 registry 获取指定设备和动作的 handles 配置
|
||||
|
||||
Args:
|
||||
resource_name: 设备资源名称,如 "liquid_handler.prcxi"
|
||||
template_name: 动作模板名称,如 "transfer_liquid"
|
||||
|
||||
Returns:
|
||||
包含 source 和 target handler_keys 的字典:
|
||||
{"source": ["sources_out", "targets_out", ...], "target": ["sources", "targets", ...]}
|
||||
"""
|
||||
result = {"source": [], "target": []}
|
||||
|
||||
device_info = lab_registry.device_type_registry.get(resource_name, {})
|
||||
if not device_info:
|
||||
return result
|
||||
|
||||
action_mappings = device_info.get("class", {}).get("action_value_mappings", {})
|
||||
action_config = action_mappings.get(template_name, {})
|
||||
handles = action_config.get("handles", {})
|
||||
|
||||
if isinstance(handles, dict):
|
||||
# 处理 input handles (作为 target)
|
||||
for handle in handles.get("input", []):
|
||||
handler_key = handle.get("handler_key", "")
|
||||
if handler_key:
|
||||
result["source"].append(handler_key)
|
||||
# 处理 output handles (作为 source)
|
||||
for handle in handles.get("output", []):
|
||||
handler_key = handle.get("handler_key", "")
|
||||
if handler_key:
|
||||
result["target"].append(handler_key)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def validate_workflow_handles(graph: WorkflowGraph) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
校验工作流图中所有边的句柄配置是否正确
|
||||
|
||||
Args:
|
||||
graph: 工作流图对象
|
||||
|
||||
Returns:
|
||||
(is_valid, errors): 是否有效,错误信息列表
|
||||
"""
|
||||
errors = []
|
||||
nodes = graph.nodes
|
||||
|
||||
for edge in graph.edges:
|
||||
left_uuid = edge.get("source")
|
||||
right_uuid = edge.get("target")
|
||||
# target_handle_key是target, right的输入节点(入节点)
|
||||
# source_handle_key是source, left的输出节点(出节点)
|
||||
right_source_conn_key = edge.get("target_handle_key", "")
|
||||
left_target_conn_key = edge.get("source_handle_key", "")
|
||||
|
||||
# 获取源节点和目标节点信息
|
||||
left_node = nodes.get(left_uuid, {})
|
||||
right_node = nodes.get(right_uuid, {})
|
||||
|
||||
left_res_name = left_node.get("resource_name", "")
|
||||
left_template_name = left_node.get("template_name", "")
|
||||
right_res_name = right_node.get("resource_name", "")
|
||||
right_template_name = right_node.get("template_name", "")
|
||||
|
||||
# 获取源节点的 output handles
|
||||
left_node_handles = get_action_handles(left_res_name, left_template_name)
|
||||
target_valid_keys = left_node_handles.get("target", [])
|
||||
target_valid_keys.append("ready")
|
||||
|
||||
# 获取目标节点的 input handles
|
||||
right_node_handles = get_action_handles(right_res_name, right_template_name)
|
||||
source_valid_keys = right_node_handles.get("source", [])
|
||||
source_valid_keys.append("ready")
|
||||
|
||||
# 如果节点配置了 output handles,则 source_port 必须有效
|
||||
if not right_source_conn_key:
|
||||
node_name = left_node.get("name", left_uuid[:8])
|
||||
errors.append(f"源节点 '{node_name}' 的 source_handle_key 为空," f"应设置为: {source_valid_keys}")
|
||||
elif right_source_conn_key not in source_valid_keys:
|
||||
node_name = left_node.get("name", left_uuid[:8])
|
||||
errors.append(
|
||||
f"源节点 '{node_name}' 的 source 端点 '{right_source_conn_key}' 不存在," f"支持的端点: {source_valid_keys}"
|
||||
)
|
||||
|
||||
# 如果节点配置了 input handles,则 target_port 必须有效
|
||||
if not left_target_conn_key:
|
||||
node_name = right_node.get("name", right_uuid[:8])
|
||||
errors.append(f"目标节点 '{node_name}' 的 target_handle_key 为空," f"应设置为: {target_valid_keys}")
|
||||
elif left_target_conn_key not in target_valid_keys:
|
||||
node_name = right_node.get("name", right_uuid[:8])
|
||||
errors.append(
|
||||
f"目标节点 '{node_name}' 的 target 端点 '{left_target_conn_key}' 不存在,"
|
||||
f"支持的端点: {target_valid_keys}"
|
||||
)
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
|
||||
# action 到 resource_name 的映射
|
||||
ACTION_RESOURCE_MAPPING: Dict[str, str] = {
|
||||
# 生物实验操作
|
||||
"transfer_liquid": "liquid_handler.prcxi",
|
||||
"transfer": "liquid_handler.prcxi",
|
||||
"incubation": "incubator.prcxi",
|
||||
"move_labware": "labware_mover.prcxi",
|
||||
"oscillation": "shaker.prcxi",
|
||||
# 有机化学操作
|
||||
"HeatChillToTemp": "heatchill.chemputer",
|
||||
"StopHeatChill": "heatchill.chemputer",
|
||||
"StartHeatChill": "heatchill.chemputer",
|
||||
"HeatChill": "heatchill.chemputer",
|
||||
"Dissolve": "stirrer.chemputer",
|
||||
"Transfer": "liquid_handler.chemputer",
|
||||
"Evaporate": "rotavap.chemputer",
|
||||
"Recrystallize": "reactor.chemputer",
|
||||
"Filter": "filter.chemputer",
|
||||
"Dry": "dryer.chemputer",
|
||||
"Add": "liquid_handler.chemputer",
|
||||
}
|
||||
|
||||
|
||||
def normalize_steps(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
将不同格式的步骤数据规范化为统一格式
|
||||
|
||||
支持的输入格式:
|
||||
- action + parameters
|
||||
- action + action_args
|
||||
- operation + parameters
|
||||
|
||||
Args:
|
||||
data: 原始步骤数据列表
|
||||
|
||||
Returns:
|
||||
规范化后的步骤列表,格式为 [{"action": str, "parameters": dict, "description": str?, "step_number": int?}, ...]
|
||||
"""
|
||||
normalized = []
|
||||
for idx, step in enumerate(data):
|
||||
# 获取动作名称(支持 action 或 operation 字段)
|
||||
action = step.get("action") or step.get("operation")
|
||||
if not action:
|
||||
continue
|
||||
|
||||
# 获取参数(支持 parameters 或 action_args 字段)
|
||||
raw_params = step.get("parameters") or step.get("action_args") or {}
|
||||
params = dict(raw_params)
|
||||
|
||||
# 规范化 source/target -> sources/targets
|
||||
if "source" in raw_params and "sources" not in raw_params:
|
||||
params["sources"] = raw_params["source"]
|
||||
if "target" in raw_params and "targets" not in raw_params:
|
||||
params["targets"] = raw_params["target"]
|
||||
|
||||
# 获取描述(支持 description 或 purpose 字段)
|
||||
description = step.get("description") or step.get("purpose")
|
||||
|
||||
# 获取步骤编号(优先使用原始数据中的 step_number,否则使用索引+1)
|
||||
step_number = step.get("step_number", idx + 1)
|
||||
|
||||
step_dict = {"action": action, "parameters": params, "step_number": step_number}
|
||||
if description:
|
||||
step_dict["description"] = description
|
||||
|
||||
normalized.append(step_dict)
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_labware(data: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
将不同格式的 labware 数据规范化为统一的字典格式
|
||||
|
||||
支持的输入格式:
|
||||
- reagent_name + material_name + positions
|
||||
- name + labware + slot
|
||||
|
||||
Args:
|
||||
data: 原始 labware 数据列表
|
||||
|
||||
Returns:
|
||||
规范化后的 labware 字典,格式为 {name: {"slot": int, "labware": str, "well": list, "type": str, "role": str, "name": str}, ...}
|
||||
"""
|
||||
labware = {}
|
||||
for item in data:
|
||||
# 获取 key 名称(优先使用 reagent_name,其次是 material_name 或 name)
|
||||
reagent_name = item.get("reagent_name")
|
||||
key = reagent_name or item.get("material_name") or item.get("name")
|
||||
if not key:
|
||||
continue
|
||||
|
||||
key = str(key)
|
||||
|
||||
# 处理重复 key,自动添加后缀
|
||||
idx = 1
|
||||
original_key = key
|
||||
while key in labware:
|
||||
idx += 1
|
||||
key = f"{original_key}_{idx}"
|
||||
|
||||
labware[key] = {
|
||||
"slot": item.get("positions") or item.get("slot"),
|
||||
"labware": item.get("material_name") or item.get("labware"),
|
||||
"well": item.get("well", []),
|
||||
"type": item.get("type", "reagent"),
|
||||
"role": item.get("role", ""),
|
||||
"name": key,
|
||||
}
|
||||
|
||||
return labware
|
||||
|
||||
|
||||
def convert_from_json(
|
||||
data: Union[str, PathLike, Dict[str, Any]],
|
||||
workstation_name: str = "PRCXi",
|
||||
validate: bool = True,
|
||||
) -> WorkflowGraph:
|
||||
"""
|
||||
从 JSON 数据或文件转换为 WorkflowGraph
|
||||
|
||||
支持的 JSON 格式:
|
||||
1. {"workflow": [...], "reagent": {...}} - 直接格式
|
||||
2. {"steps_info": [...], "labware_info": [...]} - 需要规范化的格式
|
||||
|
||||
Args:
|
||||
data: JSON 文件路径、字典数据、或 JSON 字符串
|
||||
workstation_name: 工作站名称,默认 "PRCXi"
|
||||
validate: 是否校验句柄配置,默认 True
|
||||
|
||||
Returns:
|
||||
WorkflowGraph: 构建好的工作流图
|
||||
|
||||
Raises:
|
||||
ValueError: 不支持的 JSON 格式 或 句柄校验失败
|
||||
FileNotFoundError: 文件不存在
|
||||
json.JSONDecodeError: JSON 解析失败
|
||||
"""
|
||||
# 处理输入数据
|
||||
if isinstance(data, (str, PathLike)):
|
||||
path = Path(data)
|
||||
if path.exists():
|
||||
with path.open("r", encoding="utf-8") as fp:
|
||||
json_data = json.load(fp)
|
||||
elif isinstance(data, str):
|
||||
# 尝试作为 JSON 字符串解析
|
||||
json_data = json.loads(data)
|
||||
else:
|
||||
raise FileNotFoundError(f"文件不存在: {data}")
|
||||
elif isinstance(data, dict):
|
||||
json_data = data
|
||||
else:
|
||||
raise TypeError(f"不支持的数据类型: {type(data)}")
|
||||
|
||||
# 根据格式解析数据
|
||||
if "workflow" in json_data and "reagent" in json_data:
|
||||
# 格式1: workflow/reagent(已经是规范格式)
|
||||
protocol_steps = json_data["workflow"]
|
||||
labware_info = json_data["reagent"]
|
||||
elif "steps_info" in json_data and "labware_info" in json_data:
|
||||
# 格式2: steps_info/labware_info(需要规范化)
|
||||
protocol_steps = normalize_steps(json_data["steps_info"])
|
||||
labware_info = normalize_labware(json_data["labware_info"])
|
||||
elif "steps" in json_data and "labware" in json_data:
|
||||
# 格式3: steps/labware(另一种常见格式)
|
||||
protocol_steps = normalize_steps(json_data["steps"])
|
||||
if isinstance(json_data["labware"], list):
|
||||
labware_info = normalize_labware(json_data["labware"])
|
||||
else:
|
||||
labware_info = json_data["labware"]
|
||||
else:
|
||||
raise ValueError(
|
||||
"不支持的 JSON 格式。支持的格式:\n"
|
||||
"1. {'workflow': [...], 'reagent': {...}}\n"
|
||||
"2. {'steps_info': [...], 'labware_info': [...]}\n"
|
||||
"3. {'steps': [...], 'labware': [...]}"
|
||||
)
|
||||
|
||||
# 构建工作流图
|
||||
graph = build_protocol_graph(
|
||||
labware_info=labware_info,
|
||||
protocol_steps=protocol_steps,
|
||||
workstation_name=workstation_name,
|
||||
action_resource_mapping=ACTION_RESOURCE_MAPPING,
|
||||
)
|
||||
|
||||
# 校验句柄配置
|
||||
if validate:
|
||||
is_valid, errors = validate_workflow_handles(graph)
|
||||
if not is_valid:
|
||||
import warnings
|
||||
|
||||
for error in errors:
|
||||
warnings.warn(f"句柄校验警告: {error}")
|
||||
|
||||
return graph
|
||||
|
||||
|
||||
def convert_json_to_node_link(
|
||||
data: Union[str, PathLike, Dict[str, Any]],
|
||||
workstation_name: str = "PRCXi",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
将 JSON 数据转换为 node-link 格式的字典
|
||||
|
||||
Args:
|
||||
data: JSON 文件路径、字典数据、或 JSON 字符串
|
||||
workstation_name: 工作站名称,默认 "PRCXi"
|
||||
|
||||
Returns:
|
||||
Dict: node-link 格式的工作流数据
|
||||
"""
|
||||
graph = convert_from_json(data, workstation_name)
|
||||
return graph.to_node_link_dict()
|
||||
|
||||
|
||||
def convert_json_to_workflow_list(
|
||||
data: Union[str, PathLike, Dict[str, Any]],
|
||||
workstation_name: str = "PRCXi",
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
将 JSON 数据转换为工作流列表格式
|
||||
|
||||
Args:
|
||||
data: JSON 文件路径、字典数据、或 JSON 字符串
|
||||
workstation_name: 工作站名称,默认 "PRCXi"
|
||||
|
||||
Returns:
|
||||
List: 工作流节点列表
|
||||
"""
|
||||
graph = convert_from_json(data, workstation_name)
|
||||
return graph.to_dict()
|
||||
|
||||
|
||||
# 为了向后兼容,保留下划线前缀的别名
|
||||
_normalize_steps = normalize_steps
|
||||
_normalize_labware = normalize_labware
|
||||
241
unilabos/workflow/from_python_script.py
Normal file
241
unilabos/workflow/from_python_script.py
Normal file
@@ -0,0 +1,241 @@
|
||||
import ast
|
||||
import json
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
|
||||
from .common import WorkflowGraph, RegistryAdapter
|
||||
|
||||
Json = Dict[str, Any]
|
||||
|
||||
# ---------------- Converter ----------------
|
||||
|
||||
class DeviceMethodConverter:
|
||||
"""
|
||||
- 字段统一:resource_name(原 device_class)、template_name(原 action_key)
|
||||
- params 单层;inputs 使用 'params.' 前缀
|
||||
- SimpleGraph.add_workflow_node 负责变量连线与边
|
||||
"""
|
||||
def __init__(self, device_registry: Optional[Dict[str, Any]] = None):
|
||||
self.graph = WorkflowGraph()
|
||||
self.variable_sources: Dict[str, Dict[str, Any]] = {} # var -> {node_id, output_name}
|
||||
self.instance_to_resource: Dict[str, Optional[str]] = {} # 实例名 -> resource_name
|
||||
self.node_id_counter: int = 0
|
||||
self.registry = RegistryAdapter(device_registry or {})
|
||||
|
||||
# ---- helpers ----
|
||||
def _new_node_id(self) -> int:
|
||||
nid = self.node_id_counter
|
||||
self.node_id_counter += 1
|
||||
return nid
|
||||
|
||||
def _assign_targets(self, targets) -> List[str]:
|
||||
names: List[str] = []
|
||||
import ast
|
||||
if isinstance(targets, ast.Tuple):
|
||||
for elt in targets.elts:
|
||||
if isinstance(elt, ast.Name):
|
||||
names.append(elt.id)
|
||||
elif isinstance(targets, ast.Name):
|
||||
names.append(targets.id)
|
||||
return names
|
||||
|
||||
def _extract_device_instantiation(self, node) -> Optional[Tuple[str, str]]:
|
||||
import ast
|
||||
if not isinstance(node.value, ast.Call):
|
||||
return None
|
||||
callee = node.value.func
|
||||
if isinstance(callee, ast.Name):
|
||||
class_name = callee.id
|
||||
elif isinstance(callee, ast.Attribute) and isinstance(callee.value, ast.Name):
|
||||
class_name = callee.attr
|
||||
else:
|
||||
return None
|
||||
if isinstance(node.targets[0], ast.Name):
|
||||
instance = node.targets[0].id
|
||||
return instance, class_name
|
||||
return None
|
||||
|
||||
def _extract_call(self, call) -> Tuple[str, str, Dict[str, Any], str]:
|
||||
import ast
|
||||
owner_name, method_name, call_kind = "", "", "func"
|
||||
if isinstance(call.func, ast.Attribute):
|
||||
method_name = call.func.attr
|
||||
if isinstance(call.func.value, ast.Name):
|
||||
owner_name = call.func.value.id
|
||||
call_kind = "instance" if owner_name in self.instance_to_resource else "class_or_module"
|
||||
elif isinstance(call.func.value, ast.Attribute) and isinstance(call.func.value.value, ast.Name):
|
||||
owner_name = call.func.value.attr
|
||||
call_kind = "class_or_module"
|
||||
elif isinstance(call.func, ast.Name):
|
||||
method_name = call.func.id
|
||||
call_kind = "func"
|
||||
|
||||
def pack(node):
|
||||
if isinstance(node, ast.Name):
|
||||
return {"type": "variable", "value": node.id}
|
||||
if isinstance(node, ast.Constant):
|
||||
return {"type": "constant", "value": node.value}
|
||||
if isinstance(node, ast.Dict):
|
||||
return {"type": "dict", "value": self._parse_dict(node)}
|
||||
if isinstance(node, ast.List):
|
||||
return {"type": "list", "value": self._parse_list(node)}
|
||||
return {"type": "raw", "value": ast.unparse(node) if hasattr(ast, "unparse") else str(node)}
|
||||
|
||||
args: Dict[str, Any] = {}
|
||||
pos: List[Any] = []
|
||||
for a in call.args:
|
||||
pos.append(pack(a))
|
||||
for kw in call.keywords:
|
||||
args[kw.arg] = pack(kw.value)
|
||||
if pos:
|
||||
args["_positional"] = pos
|
||||
return owner_name, method_name, args, call_kind
|
||||
|
||||
def _parse_dict(self, node) -> Dict[str, Any]:
|
||||
import ast
|
||||
out: Dict[str, Any] = {}
|
||||
for k, v in zip(node.keys, node.values):
|
||||
if isinstance(k, ast.Constant):
|
||||
key = str(k.value)
|
||||
if isinstance(v, ast.Name):
|
||||
out[key] = f"var:{v.id}"
|
||||
elif isinstance(v, ast.Constant):
|
||||
out[key] = v.value
|
||||
elif isinstance(v, ast.Dict):
|
||||
out[key] = self._parse_dict(v)
|
||||
elif isinstance(v, ast.List):
|
||||
out[key] = self._parse_list(v)
|
||||
return out
|
||||
|
||||
def _parse_list(self, node) -> List[Any]:
|
||||
import ast
|
||||
out: List[Any] = []
|
||||
for elt in node.elts:
|
||||
if isinstance(elt, ast.Name):
|
||||
out.append(f"var:{elt.id}")
|
||||
elif isinstance(elt, ast.Constant):
|
||||
out.append(elt.value)
|
||||
elif isinstance(elt, ast.Dict):
|
||||
out.append(self._parse_dict(elt))
|
||||
elif isinstance(elt, ast.List):
|
||||
out.append(self._parse_list(elt))
|
||||
return out
|
||||
|
||||
def _normalize_var_tokens(self, x: Any) -> Any:
|
||||
if isinstance(x, str) and x.startswith("var:"):
|
||||
return {"__var__": x[4:]}
|
||||
if isinstance(x, list):
|
||||
return [self._normalize_var_tokens(i) for i in x]
|
||||
if isinstance(x, dict):
|
||||
return {k: self._normalize_var_tokens(v) for k, v in x.items()}
|
||||
return x
|
||||
|
||||
def _make_params_payload(self, resource_name: Optional[str], template_name: str, call_args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
input_keys = self.registry.get_action_input_keys(resource_name, template_name) if resource_name else []
|
||||
defaults = self.registry.get_action_goal_default(resource_name, template_name) if resource_name else {}
|
||||
params: Dict[str, Any] = dict(defaults)
|
||||
|
||||
def unpack(p):
|
||||
t, v = p.get("type"), p.get("value")
|
||||
if t == "variable":
|
||||
return {"__var__": v}
|
||||
if t == "dict":
|
||||
return self._normalize_var_tokens(v)
|
||||
if t == "list":
|
||||
return self._normalize_var_tokens(v)
|
||||
return v
|
||||
|
||||
for k, p in call_args.items():
|
||||
if k == "_positional":
|
||||
continue
|
||||
params[k] = unpack(p)
|
||||
|
||||
pos = call_args.get("_positional", [])
|
||||
if pos:
|
||||
if input_keys:
|
||||
for i, p in enumerate(pos):
|
||||
if i >= len(input_keys):
|
||||
break
|
||||
name = input_keys[i]
|
||||
if name in params:
|
||||
continue
|
||||
params[name] = unpack(p)
|
||||
else:
|
||||
for i, p in enumerate(pos):
|
||||
params[f"arg_{i}"] = unpack(p)
|
||||
return params
|
||||
|
||||
# ---- handlers ----
|
||||
def _on_assign(self, stmt):
|
||||
import ast
|
||||
inst = self._extract_device_instantiation(stmt)
|
||||
if inst:
|
||||
instance, code_class = inst
|
||||
resource_name = self.registry.resolve_resource_by_classname(code_class)
|
||||
self.instance_to_resource[instance] = resource_name
|
||||
return
|
||||
|
||||
if isinstance(stmt.value, ast.Call):
|
||||
owner, method, call_args, kind = self._extract_call(stmt.value)
|
||||
if kind == "instance":
|
||||
device_key = owner
|
||||
resource_name = self.instance_to_resource.get(owner)
|
||||
else:
|
||||
device_key = owner
|
||||
resource_name = self.registry.resolve_resource_by_classname(owner)
|
||||
|
||||
module = self.registry.get_device_module(resource_name)
|
||||
params = self._make_params_payload(resource_name, method, call_args)
|
||||
|
||||
nid = self._new_node_id()
|
||||
self.graph.add_workflow_node(
|
||||
nid,
|
||||
device_key=device_key,
|
||||
resource_name=resource_name, # ✅
|
||||
module=module,
|
||||
template_name=method, # ✅
|
||||
params=params,
|
||||
variable_sources=self.variable_sources,
|
||||
add_ready_if_no_vars=True,
|
||||
prev_node_id=(nid - 1) if nid > 0 else None,
|
||||
)
|
||||
|
||||
out_vars = self._assign_targets(stmt.targets[0])
|
||||
for var in out_vars:
|
||||
self.variable_sources[var] = {"node_id": nid, "output_name": "result"}
|
||||
|
||||
def _on_expr(self, stmt):
|
||||
import ast
|
||||
if not isinstance(stmt.value, ast.Call):
|
||||
return
|
||||
owner, method, call_args, kind = self._extract_call(stmt.value)
|
||||
if kind == "instance":
|
||||
device_key = owner
|
||||
resource_name = self.instance_to_resource.get(owner)
|
||||
else:
|
||||
device_key = owner
|
||||
resource_name = self.registry.resolve_resource_by_classname(owner)
|
||||
|
||||
module = self.registry.get_device_module(resource_name)
|
||||
params = self._make_params_payload(resource_name, method, call_args)
|
||||
|
||||
nid = self._new_node_id()
|
||||
self.graph.add_workflow_node(
|
||||
nid,
|
||||
device_key=device_key,
|
||||
resource_name=resource_name, # ✅
|
||||
module=module,
|
||||
template_name=method, # ✅
|
||||
params=params,
|
||||
variable_sources=self.variable_sources,
|
||||
add_ready_if_no_vars=True,
|
||||
prev_node_id=(nid - 1) if nid > 0 else None,
|
||||
)
|
||||
|
||||
def convert(self, python_code: str):
|
||||
tree = ast.parse(python_code)
|
||||
for stmt in tree.body:
|
||||
if isinstance(stmt, ast.Assign):
|
||||
self._on_assign(stmt)
|
||||
elif isinstance(stmt, ast.Expr):
|
||||
self._on_expr(stmt)
|
||||
return self
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user