mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
Feature/xprbalance-zhida (#80)
* feat(devices): add Zhida GC/MS pretreatment automation workstation * feat(devices): add mettler_toledo xpr balance * balance
This commit is contained in:
400
unilabos/devices/zhida_gcms/zhida.py
Normal file
400
unilabos/devices/zhida_gcms/zhida.py
Normal file
@@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
智达GCMS设备驱动
|
||||
|
||||
支持智达GCMS设备的TCP通信协议,包括状态查询、方法获取、样品分析等功能。
|
||||
通信协议版本:1.0.1
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ZhidaClient:
|
||||
def __init__(self, host='192.168.3.184', port=5792, timeout=10.0):
|
||||
# 如果部署在智达GCMS上位机本地,可使用localhost: host='127.0.0.1'
|
||||
"""
|
||||
初始化智达GCMS客户端
|
||||
|
||||
Args:
|
||||
host (str): 设备IP地址,本地部署时可使用'127.0.0.1'
|
||||
port (int): 通信端口,默认5792
|
||||
timeout (float): 超时时间,单位秒
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.sock = None
|
||||
self._ros_node = None # ROS节点引用,由框架设置
|
||||
|
||||
def post_init(self, ros_node):
|
||||
"""
|
||||
ROS节点初始化后的回调方法,用于建立设备连接
|
||||
|
||||
Args:
|
||||
ros_node: ROS节点实例
|
||||
"""
|
||||
self._ros_node = ros_node
|
||||
try:
|
||||
self.connect()
|
||||
ros_node.lab_logger().info(f"智达GCMS设备连接成功: {self.host}:{self.port}")
|
||||
except Exception as e:
|
||||
ros_node.lab_logger().error(f"智达GCMS设备连接失败: {e}")
|
||||
# 不抛出异常,允许节点继续运行,后续可以重试连接
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
建立TCP连接到智达GCMS设备
|
||||
|
||||
Raises:
|
||||
ConnectionError: 连接失败时抛出
|
||||
"""
|
||||
try:
|
||||
self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
|
||||
# 确保后续 recv/send 都会在 timeout 秒后抛 socket.timeout
|
||||
self.sock.settimeout(self.timeout)
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to connect to {self.host}:{self.port} - {str(e)}")
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
关闭与智达GCMS设备的TCP连接
|
||||
"""
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.close()
|
||||
except Exception:
|
||||
pass # 忽略关闭时的错误
|
||||
finally:
|
||||
self.sock = None
|
||||
|
||||
def _send_command(self, cmd: dict) -> dict:
|
||||
"""
|
||||
发送命令到智达GCMS设备并接收响应
|
||||
|
||||
Args:
|
||||
cmd (dict): 要发送的命令字典
|
||||
|
||||
Returns:
|
||||
dict: 设备响应的JSON数据
|
||||
|
||||
Raises:
|
||||
ConnectionError: 连接错误
|
||||
TimeoutError: 超时错误
|
||||
"""
|
||||
if not self.sock:
|
||||
raise ConnectionError("Not connected to device")
|
||||
|
||||
try:
|
||||
# 发送JSON命令(UTF-8编码)
|
||||
payload = json.dumps(cmd, ensure_ascii=False).encode('utf-8')
|
||||
self.sock.sendall(payload)
|
||||
|
||||
# 循环接收数据直到能成功解析完整JSON
|
||||
buffer = bytearray()
|
||||
start = time.time()
|
||||
while True:
|
||||
try:
|
||||
chunk = self.sock.recv(4096)
|
||||
if not chunk:
|
||||
# 对端关闭连接,尝试解析已接收的数据
|
||||
if buffer:
|
||||
try:
|
||||
text = buffer.decode('utf-8', errors='strict')
|
||||
return json.loads(text)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
pass
|
||||
break
|
||||
buffer.extend(chunk)
|
||||
|
||||
# 尝试解码和解析JSON
|
||||
text = buffer.decode('utf-8', errors='strict')
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
# JSON不完整,继续接收
|
||||
pass
|
||||
|
||||
except socket.timeout:
|
||||
# 超时时,尝试解析已接收的数据
|
||||
if buffer:
|
||||
try:
|
||||
text = buffer.decode('utf-8', errors='strict')
|
||||
return json.loads(text)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
pass
|
||||
raise TimeoutError(f"recv() timed out after {self.timeout:.1f}s")
|
||||
|
||||
# 防止死循环,总时长超过2倍超时时间就报错
|
||||
if time.time() - start > self.timeout * 2:
|
||||
# 最后尝试解析已接收的数据
|
||||
if buffer:
|
||||
try:
|
||||
text = buffer.decode('utf-8', errors='strict')
|
||||
return json.loads(text)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
pass
|
||||
raise TimeoutError(f"No complete JSON received after {time.time() - start:.1f}s")
|
||||
|
||||
# 连接关闭,如果有数据则尝试解析
|
||||
if buffer:
|
||||
try:
|
||||
text = buffer.decode('utf-8', errors='strict')
|
||||
return json.loads(text)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
raise ConnectionError("Connection closed before JSON could be parsed")
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, (ConnectionError, TimeoutError)):
|
||||
raise
|
||||
else:
|
||||
raise ConnectionError(f"Command send failed: {str(e)}")
|
||||
|
||||
def get_status(self) -> str:
|
||||
"""
|
||||
获取设备状态
|
||||
|
||||
Returns:
|
||||
str: 设备状态 (Idle|Offline|Error|Busy|RunSample|Unknown)
|
||||
"""
|
||||
if not self.sock:
|
||||
# 尝试重新连接
|
||||
try:
|
||||
self.connect()
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().info("智达GCMS设备重新连接成功")
|
||||
except Exception as e:
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().warning(f"智达GCMS设备连接失败: {e}")
|
||||
return "Offline"
|
||||
|
||||
try:
|
||||
response = self._send_command({"command": "getstatus"})
|
||||
return response.get("result", "Unknown")
|
||||
except Exception as e:
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().warning(f"获取设备状态失败: {e}")
|
||||
return "Error"
|
||||
|
||||
def get_methods(self) -> dict:
|
||||
"""
|
||||
获取当前Project的方法列表
|
||||
|
||||
Returns:
|
||||
dict: 包含方法列表的响应
|
||||
"""
|
||||
if not self.sock:
|
||||
try:
|
||||
self.connect()
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().info("智达GCMS设备重新连接成功")
|
||||
except Exception as e:
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().warning(f"智达GCMS设备连接失败: {e}")
|
||||
return {"error": "Device not connected"}
|
||||
|
||||
try:
|
||||
return self._send_command({"command": "getmethods"})
|
||||
except Exception as e:
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().warning(f"获取方法列表失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
|
||||
def get_version(self) -> dict:
|
||||
"""
|
||||
获取接口版本和InLabPAL固件版本
|
||||
|
||||
Returns:
|
||||
dict: 响应格式 {"result": "OK|Error", "message": "Interface:x.x.x;FW:x.x.x.xxx"}
|
||||
"""
|
||||
return self._send_command({"command": "version"})
|
||||
|
||||
def put_tray(self) -> dict:
|
||||
"""
|
||||
放盘操作,准备InLabPAL进样器
|
||||
|
||||
注意:此功能仅在特殊场景下使用,例如:
|
||||
- 机械臂比较短,需要让开一个位置
|
||||
- 盘支架是可移动的,需要进样器配合做动作
|
||||
|
||||
对于宜宾深势这套配置,空间足够,不需要这个额外的控制组件。
|
||||
|
||||
Returns:
|
||||
dict: 响应格式 {"result": "OK|Error", "message": "ready_info|error_info"}
|
||||
"""
|
||||
return self._send_command({"command": "puttray"})
|
||||
|
||||
def start_with_csv_file(self, string: str = None, csv_file_path: str = None) -> dict:
|
||||
"""
|
||||
使用CSV文件启动分析(支持ROS2动作调用)
|
||||
|
||||
Args:
|
||||
string (str): CSV文件路径(ROS2参数名)
|
||||
csv_file_path (str): CSV文件路径(兼容旧接口)
|
||||
|
||||
Returns:
|
||||
dict: ROS2动作结果格式 {"return_info": str, "success": bool}
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: CSV文件不存在
|
||||
Exception: 文件读取或通信错误
|
||||
"""
|
||||
try:
|
||||
# 支持两种参数传递方式:ROS2的string参数和直接的csv_file_path参数
|
||||
file_path = string if string is not None else csv_file_path
|
||||
if file_path is None:
|
||||
error_msg = "未提供CSV文件路径参数"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().error(error_msg)
|
||||
return {"return_info": error_msg, "success": False}
|
||||
|
||||
# 使用Path对象进行更健壮的文件处理
|
||||
csv_path = Path(file_path)
|
||||
if not csv_path.exists():
|
||||
error_msg = f"CSV文件不存在: {file_path}"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().error(error_msg)
|
||||
return {"return_info": error_msg, "success": False}
|
||||
|
||||
# 读取CSV文件内容(UTF-8编码,替换未知字符)
|
||||
csv_content = csv_path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
# 转换为Base64编码
|
||||
b64_content = base64.b64encode(csv_content.encode('utf-8')).decode('ascii')
|
||||
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().info(f"正在发送CSV文件到智达GCMS: {file_path}")
|
||||
self._ros_node.lab_logger().info(f"Base64编码长度: {len(b64_content)} 字符")
|
||||
|
||||
# 发送start命令
|
||||
response = self._send_command({
|
||||
"command": "start",
|
||||
"message": b64_content
|
||||
})
|
||||
|
||||
# 转换为ROS2动作结果格式
|
||||
if response.get("result") == "OK":
|
||||
success_msg = f"智达GCMS分析启动成功: {response.get('message', 'Unknown')}"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().info(success_msg)
|
||||
return {"return_info": success_msg, "success": True}
|
||||
else:
|
||||
error_msg = f"智达GCMS分析启动失败: {response.get('message', 'Unknown error')}"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().error(error_msg)
|
||||
return {"return_info": error_msg, "success": False}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"CSV文件处理失败: {str(e)}"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().error(error_msg)
|
||||
return {"return_info": error_msg, "success": False}
|
||||
|
||||
def start(self, string: str = None, text: str = None) -> dict:
|
||||
"""
|
||||
使用Base64编码数据启动分析(支持ROS2动作调用)
|
||||
|
||||
Args:
|
||||
string (str): Base64编码的CSV数据(ROS2参数名)
|
||||
text (str): Base64编码的CSV数据(兼容旧接口)
|
||||
|
||||
Returns:
|
||||
dict: ROS2动作结果格式 {"return_info": str, "success": bool}
|
||||
"""
|
||||
try:
|
||||
# 支持两种参数传递方式:ROS2的string参数和原有的text参数
|
||||
b64_content = string if string is not None else text
|
||||
if b64_content is None:
|
||||
error_msg = "未提供Base64编码数据参数"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().error(error_msg)
|
||||
return {"return_info": error_msg, "success": False}
|
||||
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().info(f"正在发送Base64数据到智达GCMS")
|
||||
self._ros_node.lab_logger().info(f"Base64编码长度: {len(b64_content)} 字符")
|
||||
|
||||
# 发送start命令
|
||||
response = self._send_command({
|
||||
"command": "start",
|
||||
"message": b64_content
|
||||
})
|
||||
|
||||
# 转换为ROS2动作结果格式
|
||||
if response.get("result") == "OK":
|
||||
success_msg = f"智达GCMS分析启动成功: {response.get('message', 'Unknown')}"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().info(success_msg)
|
||||
return {"return_info": success_msg, "success": True}
|
||||
else:
|
||||
error_msg = f"智达GCMS分析启动失败: {response.get('message', 'Unknown error')}"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().error(error_msg)
|
||||
return {"return_info": error_msg, "success": False}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Base64数据处理失败: {str(e)}"
|
||||
if self._ros_node:
|
||||
self._ros_node.lab_logger().error(error_msg)
|
||||
return {"return_info": error_msg, "success": False}
|
||||
|
||||
def abort(self) -> dict:
|
||||
"""
|
||||
停止当前运行的分析
|
||||
|
||||
Returns:
|
||||
dict: 响应格式 {"result": "OK|Error", "message": "error_info"}
|
||||
"""
|
||||
return self._send_command({"command": "abort"})
|
||||
|
||||
|
||||
def test_zhida_client():
|
||||
"""
|
||||
测试智达GCMS客户端功能
|
||||
"""
|
||||
client = ZhidaClient()
|
||||
|
||||
try:
|
||||
# 连接设备
|
||||
print("Connecting to Zhida GCMS...")
|
||||
client.connect()
|
||||
print("Connected successfully!")
|
||||
|
||||
# 获取设备状态
|
||||
print(f"Device status: {client.status}")
|
||||
|
||||
# 获取版本信息
|
||||
version_info = client.get_version()
|
||||
print(f"Version info: {version_info}")
|
||||
|
||||
# 获取方法列表
|
||||
methods = client.get_methods()
|
||||
print(f"Available methods: {methods}")
|
||||
|
||||
# 测试CSV文件发送(如果文件存在)
|
||||
csv_file = Path(__file__).parent / "zhida_gcms-test_1.csv"
|
||||
if csv_file.exists():
|
||||
print(f"Testing CSV file: {csv_file}")
|
||||
result = client.start_with_csv_file(str(csv_file))
|
||||
print(f"Start result: {result}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
|
||||
finally:
|
||||
# 关闭连接
|
||||
client.close()
|
||||
print("Connection closed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_zhida_client()
|
||||
Reference in New Issue
Block a user