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:
Xie Qiming
2025-09-19 11:43:25 +08:00
committed by GitHub
parent 41eaa88c6f
commit ace98a4472
20 changed files with 2451 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
# 智达GCMS ROS2使用指南 / Zhida GCMS ROS2 User Guide
## 概述 / Overview
智达GCMS设备支持通过ROS2动作进行操作包括CSV文件分析启动、设备状态查询等功能。
The Zhida GCMS device supports operations through ROS2 actions, including CSV file analysis startup, device status queries, and other functions.
## 主要功能 / Main Features
### 1. CSV文件分析启动 / CSV File Analysis Startup (`start_with_csv_file`)
- **功能 / Function**: 接收CSV文件路径自动读取文件内容并启动分析 / Receives CSV file path, automatically reads file content and starts analysis
- **输入 / Input**: CSV文件的绝对路径 / Absolute path of CSV file
- **输出 / Output**: `{"return_info": str, "success": bool}`
### 2. 设备状态查询 / Device Status Query (`get_status`)
- **功能 / Function**: 获取设备当前运行状态 / Get current device running status
- **输出 / Output**: 设备状态字符串(如"RunSample"、"Idle"等)/ Device status string (e.g., "RunSample", "Idle", etc.)
### 3. 方法列表查询 / Method List Query (`get_methods`)
- **功能 / Function**: 获取设备支持的所有方法列表 / Get all method lists supported by the device
- **输出 / Output**: 方法列表字典 / Method list dictionary
### 4. 放盘操作 / Tray Operation (`put_tray`)
- **功能 / Function**: 控制设备准备样品托盘 / Control device to prepare sample tray
- **输出 / Output**: 操作结果信息 / Operation result information
### 5. 停止运行 / Stop Operation (`abort`)
- **功能 / Function**: 中止当前正在进行的分析任务 / Abort current analysis task in progress
- **输出 / Output**: 操作结果信息 / Operation result information
### 6. 获取版本信息 / Get Version Information (`get_version`)
- **功能 / Function**: 查询设备接口版本和固件版本信息 / Query device interface version and firmware version information
- **输出 / Output**: 版本信息字典 / Version information dictionary
## 使用方法 / Usage Methods
### ROS2命令行使用 / ROS2 Command Line Usage
### 1. 查询设备状态 / Query Device Status
```bash
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/get_status unilabos_msgs/action/EmptyIn "{}"
```
### 2. 查询方法列表 / Query Method List
```bash
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/get_methods unilabos_msgs/action/EmptyIn "{}"
```
### 3. 启动分析 / Start Analysis
使用CSV文件启动分析 / Start analysis using CSV file:
```bash
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/start_with_csv_file unilabos_msgs/action/StrSingleInput "{string: 'D:/path/to/your/samples.csv'}"
```
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/start_with_csv_file unilabos_msgs/action/StrSingleInput "{string: 'd:/UniLab/Uni-Lab-OS/unilabos/devices/zhida_gcms/zhida_gcms-test_3.csv'}"
使用Base64编码数据启动分析 / Start analysis using Base64 encoded data:
```bash
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/start unilabos_msgs/action/StrSingleInput "{string: 'U2FtcGxlTmFtZSxBY3FNZXRob2QsUmFja0NvZGUsVmlhbFBvcyxTbXBsSW5qVm9sLE91dHB1dEZpbGU...'}"
```
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/start unilabos_msgs/action/StrSingleInput "{string: 'U2FtcGxlTmFtZSxBY3FNZXRob2QsUmFja0NvZGUsVmlhbFBvcyxTbXBsSW5qVm9sLE91dHB1dEZpbGUKU2FtcGxlMDAxLDIwMjUwNjA0LXRlc3QsUmFjayAxLDEsMSwvQ2hyb21lbGVvbkxvY2FsL++/veixuO+/vcSyxLzvv73vv73vv73vv73vv73vv73vv73vv73vv70vMjAyNTA2MDQK'}"
### 4. 放盘操作 / Tray Operation
**注意 / Note**: 放盘操作是特殊场景下使用的功能,比如机械臂比较短需要让开位置,或者盘支架是可移动的时候,这个指令让进样器也去做相应动作。在当前配置中,空间足够,不需要这个额外的控制组件。
**Note**: The tray operation is used in special scenarios, such as when the robotic arm is relatively short and needs to make room, or when the tray bracket is movable, this command makes the injector perform corresponding actions. In the current configuration, the space is sufficient and this additional control component is not needed.
准备样品托盘 / Prepare sample tray:
```bash
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/put_tray unilabos_msgs/action/EmptyIn "{}"
```
### 5. 停止运行 / Stop Operation
中止当前分析任务注意运行中发现任务运行中止需要人工在InLab Solution 二次点击确认)/ Abort current analysis task (Note! If task abortion is detected during operation, manual confirmation is required by clicking twice in InLab Solution):
```bash
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/abort unilabos_msgs/action/EmptyIn "{}"
```
### 6. 获取版本信息 / Get Version Information
查询设备版本 / Query device version:
```bash
ros2 action send_goal /devices/ZHIDA_GCMS_STATION/get_version unilabos_msgs/action/EmptyIn "{}"
```
### Python代码使用 / Python Code Usage
```python
from unilabos.devices.zhida_gcms.zhida import ZhidaClient
# 初始化客户端 / Initialize client
client = ZhidaClient(host='192.168.3.184', port=5792)
client.connect()
# 使用CSV文件启动分析 / Start analysis using CSV file
result = client.start_with_csv_file('/path/to/your/file.csv')
print(f"成功 / Success: {result['success']}")
print(f"信息 / Info: {result['return_info']}")
# 查询设备状态 / Query device status
status = client.get_status()
print(f"设备状态 / Device Status: {status}")
client.close()
```
## 使用注意事项 / Usage Notes
1. **文件路径 / File Path**: 必须使用绝对路径 / Must use absolute path
2. **文件格式 / File Format**: CSV文件必须是UTF-8编码 / CSV file must be UTF-8 encoded
3. **设备连接 / Device Connection**: 确保智达GCMS设备已连接并可访问 / Ensure Zhida GCMS device is connected and accessible
4. **权限 / Permissions**: 确保有读取CSV文件的权限 / Ensure you have permission to read CSV files
## 故障排除 / Troubleshooting
### 常见问题 / Common Issues
1. **文件路径错误 / File Path Error**: 确保使用绝对路径且文件存在 / Ensure using absolute path and file exists
2. **编码问题 / Encoding Issue**: 确保CSV文件是UTF-8编码 / Ensure CSV file is UTF-8 encoded
3. **设备连接 / Device Connection**: 检查网络连接和设备状态 / Check network connection and device status
4. **权限问题 / Permission Issue**: 确保有文件读取权限 / Ensure you have file read permissions
### 设备状态说明 / Device Status Description
- `"Idle"`: 设备空闲状态 / Device idle status
- `"RunSample"`: 正在运行样品分析 / Running sample analysis
- `"Error"`: 设备错误状态 / Device error status
## 总结 / Summary
智达GCMS设备现在支持 / Zhida GCMS device now supports:
1. 直接通过ROS2命令输入CSV文件路径启动分析 / Direct CSV file path input via ROS2 commands to start analysis
2. 按需查询设备状态和方法列表 / On-demand device status and method list queries
3. 完善的错误处理和日志记录 / Comprehensive error handling and logging
4. 简化的操作流程 / Simplified operation workflow

View File

View File

@@ -0,0 +1,24 @@
{
"nodes": [
{
"id": "ZHIDA_GCMS_STATION",
"name": "ZHIDA_GCMS",
"parent": null,
"type": "device",
"class": "zhida_gcms",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"host": "192.168.3.184",
"port": 5792,
"timeout": 10.0
},
"data": {},
"children": []
}
],
"links": []
}

View 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()

View File

@@ -0,0 +1,2 @@
SampleName,AcqMethod,RackCode,VialPos,SmplInjVol,OutputFile
Sample001,/ChromeleonLocal/<2F><EFBFBD>IJļ<C4B2><C4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>/20250604/20231113.seq/20250604-test,Rack 1,1,1,/ChromeleonLocal/<2F><EFBFBD>IJļ<C4B2><C4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>/20250604
1 SampleName AcqMethod RackCode VialPos SmplInjVol OutputFile
2 Sample001 /ChromeleonLocal/�豸�IJļ���������/20250604/20231113.seq/20250604-test Rack 1 1 1 /ChromeleonLocal/�豸�IJļ���������/20250604